diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..176a458f --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.gitignore b/.gitignore index df21bc61..4f6666dc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -settings.py -*.pyc -.idea/* +settings.py +*.pyc +.idea/* .cache/* \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 9b14856d..2e0feec8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -httptools -ujson +httptools +ujson uvloop \ No newline at end of file diff --git a/sanic/config.py b/sanic/config.py index 3fc174bc..8261c2c0 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -1,24 +1,24 @@ -class Config: - LOGO = """ - ▄▄▄▄▄ - ▀▀▀██████▄▄▄ _______________ - ▄▄▄▄▄ █████████▄ / \\ - ▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! | - ▀▀█████▄▄ ▀██████▄██ | _________________/ - ▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/ - ▀▀▀▄ ▀▀███ ▀ ▄▄ - ▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌ - ██▀▄▄▄██▀▄███▀ ▀▀████ ▄██ -▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀ -▌ ▐▀████▐███▒▒▒▒▒▐██▌ -▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀ - ▀▀█████████▀ - ▄▄██▀██████▀█ - ▄██▀ ▀▀▀ █ - ▄█ ▐▌ - ▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄ -▌ ▐ ▀▀▄▄▄▀ - ▀▀▄▄▀ -""" - REQUEST_MAX_SIZE = 100000000 # 100 megababies - REQUEST_TIMEOUT = 60 # 60 seconds +class Config: + LOGO = """ + ▄▄▄▄▄ + ▀▀▀██████▄▄▄ _______________ + ▄▄▄▄▄ █████████▄ / \\ + ▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! | + ▀▀█████▄▄ ▀██████▄██ | _________________/ + ▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/ + ▀▀▀▄ ▀▀███ ▀ ▄▄ + ▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌ + ██▀▄▄▄██▀▄███▀ ▀▀████ ▄██ +▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███ ▌▄▄▀ +▌ ▐▀████▐███▒▒▒▒▒▐██▌ +▀▄▄▄▄▀ ▀▀████▒▒▒▒▄██▀ + ▀▀█████████▀ + ▄▄██▀██████▀█ + ▄██▀ ▀▀▀ █ + ▄█ ▐▌ + ▄▄▄▄█▌ ▀█▄▄▄▄▀▀▄ +▌ ▐ ▀▀▄▄▄▀ + ▀▀▄▄▀ +""" + REQUEST_MAX_SIZE = 100000000 # 100 megababies + REQUEST_TIMEOUT = 60 # 60 seconds diff --git a/sanic/log.py b/sanic/log.py index b7548203..41dd56e4 100644 --- a/sanic/log.py +++ b/sanic/log.py @@ -1,4 +1,4 @@ -import logging - -logging.basicConfig(level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s") -log = logging.getLogger(__name__) +import logging + +logging.basicConfig(level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s") +log = logging.getLogger(__name__) diff --git a/sanic/request.py b/sanic/request.py index e7f0dc57..ece1ae8f 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -1,143 +1,143 @@ -from cgi import parse_header -from collections import namedtuple -from httptools import parse_url -from urllib.parse import parse_qs -from ujson import loads as json_loads - -from .log import log - - -class RequestParameters(dict): - """ - Hosts a dict with lists as values where get returns the first - value of the list and getlist returns the whole shebang - """ - - def __init__(self, *args, **kwargs): - self.super = super() - self.super.__init__(*args, **kwargs) - - def get(self, name, default=None): - values = self.super.get(name) - return values[0] if values else default - - def getlist(self, name, default=None): - return self.super.get(name, default) - - -class Request: - __slots__ = ( - 'url', 'headers', 'version', 'method', - 'query_string', 'body', - 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', - ) - - def __init__(self, url_bytes, headers, version, method): - # TODO: Content-Encoding detection - url_parsed = parse_url(url_bytes) - self.url = url_parsed.path.decode('utf-8') - self.headers = headers - self.version = version - self.method = method - self.query_string = url_parsed.query.decode('utf-8') if url_parsed.query else None - - # Init but do not inhale - self.body = None - self.parsed_json = None - self.parsed_form = None - self.parsed_files = None - self.parsed_args = None - - @property - def json(self): - if not self.parsed_json: - try: - self.parsed_json = json_loads(self.body) - except: - pass - - return self.parsed_json - - @property - def form(self): - if self.parsed_form is None: - self.parsed_form = {} - self.parsed_files = {} - content_type, parameters = parse_header(self.headers.get('Content-Type')) - try: - if content_type is None or content_type == 'application/x-www-form-urlencoded': - self.parsed_form = RequestParameters(parse_qs(self.body.decode('utf-8'))) - elif content_type == 'multipart/form-data': - # TODO: Stream this instead of reading to/from memory - boundary = parameters['boundary'].encode('utf-8') - self.parsed_form, self.parsed_files = parse_multipart_form(self.body, boundary) - except Exception as e: - log.exception(e) - pass - - return self.parsed_form - - @property - def files(self): - if self.parsed_files is None: - _ = self.form # compute form to get files - - return self.parsed_files - - @property - def args(self): - if self.parsed_args is None: - if self.query_string: - self.parsed_args = RequestParameters(parse_qs(self.query_string)) - else: - self.parsed_args = {} - - return self.parsed_args - - -File = namedtuple('File', ['type', 'body', 'name']) - - -def parse_multipart_form(body, boundary): - """ - Parses a request body and returns fields and files - :param body: Bytes request body - :param boundary: Bytes multipart boundary - :return: fields (dict), files (dict) - """ - files = {} - fields = {} - - form_parts = body.split(boundary) - for form_part in form_parts[1:-1]: - file_name = None - file_type = None - field_name = None - line_index = 2 - line_end_index = 0 - while not line_end_index == -1: - line_end_index = form_part.find(b'\r\n', line_index) - form_line = form_part[line_index:line_end_index].decode('utf-8') - line_index = line_end_index + 2 - - if not form_line: - break - - colon_index = form_line.index(':') - form_header_field = form_line[0:colon_index] - form_header_value, form_parameters = parse_header(form_line[colon_index + 2:]) - - if form_header_field == 'Content-Disposition': - if 'filename' in form_parameters: - file_name = form_parameters['filename'] - field_name = form_parameters.get('name') - elif form_header_field == 'Content-Type': - file_type = form_header_value - - post_data = form_part[line_index:-4] - if file_name or file_type: - files[field_name] = File(type=file_type, name=file_name, body=post_data) - else: - fields[field_name] = post_data.decode('utf-8') - - return fields, files +from cgi import parse_header +from collections import namedtuple +from httptools import parse_url +from urllib.parse import parse_qs +from ujson import loads as json_loads + +from .log import log + + +class RequestParameters(dict): + """ + Hosts a dict with lists as values where get returns the first + value of the list and getlist returns the whole shebang + """ + + def __init__(self, *args, **kwargs): + self.super = super() + self.super.__init__(*args, **kwargs) + + def get(self, name, default=None): + values = self.super.get(name) + return values[0] if values else default + + def getlist(self, name, default=None): + return self.super.get(name, default) + + +class Request: + __slots__ = ( + 'url', 'headers', 'version', 'method', + 'query_string', 'body', + 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', + ) + + def __init__(self, url_bytes, headers, version, method): + # TODO: Content-Encoding detection + url_parsed = parse_url(url_bytes) + self.url = url_parsed.path.decode('utf-8') + self.headers = headers + self.version = version + self.method = method + self.query_string = url_parsed.query.decode('utf-8') if url_parsed.query else None + + # Init but do not inhale + self.body = None + self.parsed_json = None + self.parsed_form = None + self.parsed_files = None + self.parsed_args = None + + @property + def json(self): + if not self.parsed_json: + try: + self.parsed_json = json_loads(self.body) + except: + pass + + return self.parsed_json + + @property + def form(self): + if self.parsed_form is None: + self.parsed_form = {} + self.parsed_files = {} + content_type, parameters = parse_header(self.headers.get('Content-Type')) + try: + if content_type is None or content_type == 'application/x-www-form-urlencoded': + self.parsed_form = RequestParameters(parse_qs(self.body.decode('utf-8'))) + elif content_type == 'multipart/form-data': + # TODO: Stream this instead of reading to/from memory + boundary = parameters['boundary'].encode('utf-8') + self.parsed_form, self.parsed_files = parse_multipart_form(self.body, boundary) + except Exception as e: + log.exception(e) + pass + + return self.parsed_form + + @property + def files(self): + if self.parsed_files is None: + _ = self.form # compute form to get files + + return self.parsed_files + + @property + def args(self): + if self.parsed_args is None: + if self.query_string: + self.parsed_args = RequestParameters(parse_qs(self.query_string)) + else: + self.parsed_args = {} + + return self.parsed_args + + +File = namedtuple('File', ['type', 'body', 'name']) + + +def parse_multipart_form(body, boundary): + """ + Parses a request body and returns fields and files + :param body: Bytes request body + :param boundary: Bytes multipart boundary + :return: fields (dict), files (dict) + """ + files = {} + fields = {} + + form_parts = body.split(boundary) + for form_part in form_parts[1:-1]: + file_name = None + file_type = None + field_name = None + line_index = 2 + line_end_index = 0 + while not line_end_index == -1: + line_end_index = form_part.find(b'\r\n', line_index) + form_line = form_part[line_index:line_end_index].decode('utf-8') + line_index = line_end_index + 2 + + if not form_line: + break + + colon_index = form_line.index(':') + form_header_field = form_line[0:colon_index] + form_header_value, form_parameters = parse_header(form_line[colon_index + 2:]) + + if form_header_field == 'Content-Disposition': + if 'filename' in form_parameters: + file_name = form_parameters['filename'] + field_name = form_parameters.get('name') + elif form_header_field == 'Content-Type': + file_type = form_header_value + + post_data = form_part[line_index:-4] + if file_name or file_type: + files[field_name] = File(type=file_type, name=file_name, body=post_data) + else: + fields[field_name] = post_data.decode('utf-8') + + return fields, files diff --git a/sanic/response.py b/sanic/response.py index c1a0569f..6807d04c 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -1,68 +1,68 @@ -import ujson -import httptools -from ujson import loads as json_loads -from urllib.parse import parse_qs - -STATUS_CODES = { - 200: 'OK', - 400: 'Bad Request', - 401: 'Unauthorized', - 402: 'Payment Required', - 403: 'Forbidden', - 404: 'Not Found', - 405: 'Method Not Allowed', - 500: 'Internal Server Error', - 501: 'Not Implemented', - 502: 'Bad Gateway', - 503: 'Service Unavailable', - 504: 'Gateway Timeout', -} - - -class HTTPResponse: - __slots__ = ('body', 'status', 'content_type', 'headers') - - def __init__(self, body=None, status=200, headers=[], content_type='text/plain', body_bytes=b''): - self.content_type = content_type - - if not body is None: - self.body = body.encode('utf-8') - else: - self.body = body_bytes - - self.status = status - self.headers = headers - - def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): - # This is all returned in a kind-of funky way - # We tried to make this as fast as possible in pure python - additional_headers = [] - 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'] - if self.headers: - for name, value in self.headers.items(): - additional_headers.append('{}: {}\r\n'.format(name, value).encode('utf-8')) - - return b''.join([ - 'HTTP/{} {} {}\r\n'.format(version, self.status, - STATUS_CODES.get(self.status, 'FAIL')).encode(), - b'Content-Type: ', self.content_type.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', - ] + additional_headers + [ - b'\r\n', - self.body, - ]) - - -def json(body, status=200, headers=None): - return HTTPResponse(ujson.dumps(body), headers=headers, status=status, - content_type="application/json; charset=utf-8") - - -def text(body, status=200, headers=None): - return HTTPResponse(body, status=status, headers=headers, content_type="text/plain; charset=utf-8") - - -def html(body, status=200, headers=None): - return HTTPResponse(body, status=status, headers=headers, content_type="text/html; charset=utf-8") +import ujson +import httptools +from ujson import loads as json_loads +from urllib.parse import parse_qs + +STATUS_CODES = { + 200: 'OK', + 400: 'Bad Request', + 401: 'Unauthorized', + 402: 'Payment Required', + 403: 'Forbidden', + 404: 'Not Found', + 405: 'Method Not Allowed', + 500: 'Internal Server Error', + 501: 'Not Implemented', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + 504: 'Gateway Timeout', +} + + +class HTTPResponse: + __slots__ = ('body', 'status', 'content_type', 'headers') + + def __init__(self, body=None, status=200, headers=[], content_type='text/plain', body_bytes=b''): + self.content_type = content_type + + if not body is None: + self.body = body.encode('utf-8') + else: + self.body = body_bytes + + self.status = status + self.headers = headers + + def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None): + # This is all returned in a kind-of funky way + # We tried to make this as fast as possible in pure python + additional_headers = [] + 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'] + if self.headers: + for name, value in self.headers.items(): + additional_headers.append('{}: {}\r\n'.format(name, value).encode('utf-8')) + + return b''.join([ + 'HTTP/{} {} {}\r\n'.format(version, self.status, + STATUS_CODES.get(self.status, 'FAIL')).encode(), + b'Content-Type: ', self.content_type.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', + ] + additional_headers + [ + b'\r\n', + self.body, + ]) + + +def json(body, status=200, headers=None): + return HTTPResponse(ujson.dumps(body), headers=headers, status=status, + content_type="application/json; charset=utf-8") + + +def text(body, status=200, headers=None): + return HTTPResponse(body, status=status, headers=headers, content_type="text/plain; charset=utf-8") + + +def html(body, status=200, headers=None): + return HTTPResponse(body, status=status, headers=headers, content_type="text/html; charset=utf-8") diff --git a/sanic/router.py b/sanic/router.py index 892ba655..ddab5c61 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -1,128 +1,128 @@ -import re -from collections import namedtuple -from .exceptions import NotFound, InvalidUsage - -Route = namedtuple("Route", ['handler', 'methods', 'pattern', 'parameters']) -Parameter = namedtuple("Parameter", ['name', 'cast']) - - -class Router: - """ - Router supports basic routing with parameters and method checks - Usage: - @sanic.route('/my/url/', methods=['GET', 'POST', ...]) - def my_route(request, my_parameter): - do stuff... - - Parameters will be passed as keyword arguments to the request handling function provided - Parameters can also have a type by appending :type to the . If no type is provided, - a string is expected. A regular expression can also be passed in as the type - - TODO: - 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 - """ - routes = None - regex_types = { - "string": (None, "\w+"), - "int": (int, "\d+"), - "number": (float, "[0-9\\.]+"), - "alpha": (None, "[A-Za-z]+"), - } - - def __init__(self): - self.routes = [] - - def add(self, uri, methods, handler): - """ - Adds a handler to the route list - :param uri: Path to match - :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. - :return: Nothing - """ - - # Dict for faster lookups of if method allowed - methods_dict = {method: True for method in methods} if methods else None - - parameters = [] - - def add_parameter(match): - # We could receive NAME or NAME:PATTERN - parts = match.group(1).split(':') - if len(parts) == 2: - parameter_name, parameter_pattern = parts - else: - parameter_name = parts[0] - parameter_pattern = 'string' - - # Pull from pre-configured types - parameter_regex = self.regex_types.get(parameter_pattern) - if parameter_regex: - parameter_type, parameter_pattern = parameter_regex - else: - parameter_type = None - - parameter = Parameter(name=parameter_name, cast=parameter_type) - parameters.append(parameter) - - return "({})".format(parameter_pattern) - - pattern_string = re.sub("<(.+?)>", add_parameter, uri) - pattern = re.compile("^{}$".format(pattern_string)) - - route = Route(handler=handler, methods=methods_dict, pattern=pattern, parameters=parameters) - self.routes.append(route) - - def get(self, request): - """ - Gets a request handler based on the URL of the request, or raises an error - :param request: Request object - :return: handler, arguments, keyword arguments - """ - - route = None - args = [] - kwargs = {} - for _route in self.routes: - match = _route.pattern.match(request.url) - if match: - for index, parameter in enumerate(_route.parameters, start=1): - value = match.group(index) - kwargs[parameter.name] = parameter.cast(value) if parameter.cast is not None else value - route = _route - break - - if route: - if route.methods and not request.method in route.methods: - raise InvalidUsage("Method {} not allowed for URL {}".format(request.method, request.url), - status_code=405) - return route.handler, args, kwargs - else: - raise NotFound("Requested URL {} not found".format(request.url)) - - -class SimpleRouter: - """ - Simple router records and reads all routes from a dictionary - It does not support parameters in routes, but is very fast - """ - routes = None - - def __init__(self): - self.routes = {} - - def add(self, uri, methods, handler): - # Dict for faster lookups of method allowed - 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) - - def get(self, request): - route = self.routes.get(request.url) - if route: - if route.methods and not request.method in route.methods: - raise InvalidUsage("Method {} not allowed for URL {}".format(request.method, request.url), - status_code=405) - return route.handler, [], {} - else: - raise NotFound("Requested URL {} not found".format(request.url)) +import re +from collections import namedtuple +from .exceptions import NotFound, InvalidUsage + +Route = namedtuple("Route", ['handler', 'methods', 'pattern', 'parameters']) +Parameter = namedtuple("Parameter", ['name', 'cast']) + + +class Router: + """ + Router supports basic routing with parameters and method checks + Usage: + @sanic.route('/my/url/', methods=['GET', 'POST', ...]) + def my_route(request, my_parameter): + do stuff... + + Parameters will be passed as keyword arguments to the request handling function provided + Parameters can also have a type by appending :type to the . If no type is provided, + a string is expected. A regular expression can also be passed in as the type + + TODO: + 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 + """ + routes = None + regex_types = { + "string": (None, "\w+"), + "int": (int, "\d+"), + "number": (float, "[0-9\\.]+"), + "alpha": (None, "[A-Za-z]+"), + } + + def __init__(self): + self.routes = [] + + def add(self, uri, methods, handler): + """ + Adds a handler to the route list + :param uri: Path to match + :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. + :return: Nothing + """ + + # Dict for faster lookups of if method allowed + methods_dict = {method: True for method in methods} if methods else None + + parameters = [] + + def add_parameter(match): + # We could receive NAME or NAME:PATTERN + parts = match.group(1).split(':') + if len(parts) == 2: + parameter_name, parameter_pattern = parts + else: + parameter_name = parts[0] + parameter_pattern = 'string' + + # Pull from pre-configured types + parameter_regex = self.regex_types.get(parameter_pattern) + if parameter_regex: + parameter_type, parameter_pattern = parameter_regex + else: + parameter_type = None + + parameter = Parameter(name=parameter_name, cast=parameter_type) + parameters.append(parameter) + + return "({})".format(parameter_pattern) + + pattern_string = re.sub("<(.+?)>", add_parameter, uri) + pattern = re.compile("^{}$".format(pattern_string)) + + route = Route(handler=handler, methods=methods_dict, pattern=pattern, parameters=parameters) + self.routes.append(route) + + def get(self, request): + """ + Gets a request handler based on the URL of the request, or raises an error + :param request: Request object + :return: handler, arguments, keyword arguments + """ + + route = None + args = [] + kwargs = {} + for _route in self.routes: + match = _route.pattern.match(request.url) + if match: + for index, parameter in enumerate(_route.parameters, start=1): + value = match.group(index) + kwargs[parameter.name] = parameter.cast(value) if parameter.cast is not None else value + route = _route + break + + if route: + if route.methods and not request.method in route.methods: + raise InvalidUsage("Method {} not allowed for URL {}".format(request.method, request.url), + status_code=405) + return route.handler, args, kwargs + else: + raise NotFound("Requested URL {} not found".format(request.url)) + + +class SimpleRouter: + """ + Simple router records and reads all routes from a dictionary + It does not support parameters in routes, but is very fast + """ + routes = None + + def __init__(self): + self.routes = {} + + def add(self, uri, methods, handler): + # Dict for faster lookups of method allowed + 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) + + def get(self, request): + route = self.routes.get(request.url) + if route: + if route.methods and not request.method in route.methods: + raise InvalidUsage("Method {} not allowed for URL {}".format(request.method, request.url), + status_code=405) + return route.handler, [], {} + else: + raise NotFound("Requested URL {} not found".format(request.url)) diff --git a/sanic/sanic.py b/sanic/sanic.py index f17cd4af..8fd1c6b7 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -1,191 +1,191 @@ -import asyncio -from inspect import isawaitable -from traceback import format_exc -from types import FunctionType - -from .config import Config -from .exceptions import Handler -from .log import log, logging -from .middleware import Middleware -from .response import HTTPResponse -from .router import Router -from .server import serve -from .exceptions import ServerError - - -class Sanic: - def __init__(self, name, router=None, error_handler=None): - self.name = name - self.router = router or Router() - self.router = router or Router() - self.error_handler = error_handler or Handler(self) - self.config = Config() - self.request_middleware = [] - self.response_middleware = [] - - # -------------------------------------------------------------------- # - # 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 - """ - - def response(handler): - self.router.add(uri=uri, methods=methods, handler=handler) - return handler - - return response - - # Decorator - def exception(self, *exceptions): - """ - 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 - """ - - 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') - """ - middleware = None - attach_to = 'request' - - def register_middleware(middleware): - if attach_to == 'request': - self.request_middleware.append(middleware) - if attach_to == 'response': - self.response_middleware.append(middleware) - 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 - - if isinstance(middleware, FunctionType): - middleware = Middleware(process_request=middleware) - - return middleware - - # -------------------------------------------------------------------- # - # Request Handling - # -------------------------------------------------------------------- # - - async def handle_request(self, request, response_callback): - """ - 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 - :param request: HTTP Request object - :param response_callback: Response function to be called with the response as the only argument - :return: Nothing - """ - try: - # Middleware process_request - 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: - # Fetch handler from router - handler, args, kwargs = self.router.get(request) - if handler is None: - raise ServerError("'None' was returned while requesting a handler from the router") - - # Run response handler - response = handler(request, *args, **kwargs) - if isawaitable(response): - response = await response - - # Middleware process_response - 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 - - except Exception as e: - try: - response = self.error_handler.response(request, e) - if isawaitable(response): - response = await response - except Exception as e: - if self.debug: - response = HTTPResponse("Error while handling error: {}\nStack: {}".format(e, format_exc())) - else: - response = HTTPResponse("An error occured while handling an error") - - response_callback(response) - - # -------------------------------------------------------------------- # - # Execution - # -------------------------------------------------------------------- # - - 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. - On termination, drains connections before closing. - :param host: Address to host on - :param port: Port to host on - :param debug: Enables debug output (slows server) - :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 - :return: Nothing - """ - self.error_handler.debug = True - self.debug = debug - - if debug: - log.setLevel(logging.DEBUG) - log.debug(self.config.LOGO) - - # Serve - log.info('Goin\' Fast @ http://{}:{}'.format(host, port)) - - try: - serve( - host=host, - port=port, - debug=debug, - after_start=after_start, - before_stop=before_stop, - request_handler=self.handle_request, - request_timeout=self.config.REQUEST_TIMEOUT, - request_max_size=self.config.REQUEST_MAX_SIZE, - ) - except: - pass - - def stop(self): - """ - This kills the Sanic - """ - asyncio.get_event_loop().stop() +import asyncio +from inspect import isawaitable +from traceback import format_exc +from types import FunctionType + +from .config import Config +from .exceptions import Handler +from .log import log, logging +from .middleware import Middleware +from .response import HTTPResponse +from .router import Router +from .server import serve +from .exceptions import ServerError + + +class Sanic: + def __init__(self, name, router=None, error_handler=None): + self.name = name + self.router = router or Router() + self.router = router or Router() + self.error_handler = error_handler or Handler(self) + self.config = Config() + self.request_middleware = [] + self.response_middleware = [] + + # -------------------------------------------------------------------- # + # 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 + """ + + def response(handler): + self.router.add(uri=uri, methods=methods, handler=handler) + return handler + + return response + + # Decorator + def exception(self, *exceptions): + """ + 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 + """ + + 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') + """ + middleware = None + attach_to = 'request' + + def register_middleware(middleware): + if attach_to == 'request': + self.request_middleware.append(middleware) + if attach_to == 'response': + self.response_middleware.append(middleware) + 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 + + if isinstance(middleware, FunctionType): + middleware = Middleware(process_request=middleware) + + return middleware + + # -------------------------------------------------------------------- # + # Request Handling + # -------------------------------------------------------------------- # + + async def handle_request(self, request, response_callback): + """ + 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 + :param request: HTTP Request object + :param response_callback: Response function to be called with the response as the only argument + :return: Nothing + """ + try: + # Middleware process_request + 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: + # Fetch handler from router + handler, args, kwargs = self.router.get(request) + if handler is None: + raise ServerError("'None' was returned while requesting a handler from the router") + + # Run response handler + response = handler(request, *args, **kwargs) + if isawaitable(response): + response = await response + + # Middleware process_response + 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 + + except Exception as e: + try: + response = self.error_handler.response(request, e) + if isawaitable(response): + response = await response + except Exception as e: + if self.debug: + response = HTTPResponse("Error while handling error: {}\nStack: {}".format(e, format_exc())) + else: + response = HTTPResponse("An error occured while handling an error") + + response_callback(response) + + # -------------------------------------------------------------------- # + # Execution + # -------------------------------------------------------------------- # + + 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. + On termination, drains connections before closing. + :param host: Address to host on + :param port: Port to host on + :param debug: Enables debug output (slows server) + :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 + :return: Nothing + """ + self.error_handler.debug = True + self.debug = debug + + if debug: + log.setLevel(logging.DEBUG) + log.debug(self.config.LOGO) + + # Serve + log.info('Goin\' Fast @ http://{}:{}'.format(host, port)) + + try: + serve( + host=host, + port=port, + debug=debug, + after_start=after_start, + before_stop=before_stop, + request_handler=self.handle_request, + request_timeout=self.config.REQUEST_TIMEOUT, + request_max_size=self.config.REQUEST_MAX_SIZE, + ) + except: + pass + + def stop(self): + """ + This kills the Sanic + """ + asyncio.get_event_loop().stop() diff --git a/sanic/server.py b/sanic/server.py index 338435c8..5704bccf 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -1,205 +1,205 @@ -import asyncio -from inspect import isawaitable -from signal import SIGINT, SIGTERM - -import httptools - -try: - import uvloop as async_loop -except: - async_loop = asyncio - -from .log import log -from .request import Request - - -class Signal: - stopped = False - - -class HttpProtocol(asyncio.Protocol): - __slots__ = ('loop', 'transport', 'connections', 'signal', # event loop, connection - 'parser', 'request', 'url', 'headers', # request params - 'request_handler', 'request_timeout', 'request_max_size', # request config - '_total_request_size', '_timeout_handler') # connection management - - def __init__(self, *, loop, request_handler, signal=Signal(), connections={}, request_timeout=60, - request_max_size=None): - self.loop = loop - self.transport = None - self.request = None - self.parser = None - self.url = None - self.headers = None - self.signal = signal - self.connections = connections - self.request_handler = request_handler - self.request_timeout = request_timeout - self.request_max_size = request_max_size - self._total_request_size = 0 - self._timeout_handler = None - - # -------------------------------------------- # - - # Connection - # -------------------------------------------- # - - def connection_made(self, transport): - self.connections[self] = True - self._timeout_handler = self.loop.call_later(self.request_timeout, self.connection_timeout) - self.transport = transport - - def connection_lost(self, exc): - del self.connections[self] - self._timeout_handler.cancel() - self.cleanup() - - def connection_timeout(self): - self.bail_out("Request timed out, connection closed") - - # -------------------------------------------- # - - # Parsing - # -------------------------------------------- # - - def data_received(self, data): - # Check for the request itself getting too large and exceeding memory limits - self._total_request_size += len(data) - if self._total_request_size > self.request_max_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 - if self.parser is None: - assert self.request is None - self.headers = [] - self.parser = httptools.HttpRequestParser(self) - - # Parse request chunk or close connection - try: - self.parser.feed_data(data) - except httptools.parser.errors.HttpParserError as e: - self.bail_out("Invalid request data, connection closed ({})".format(e)) - - def on_url(self, url): - self.url = url - - def on_header(self, name, value): - if name == b'Content-Length' and int(value) > self.request_max_size: - return self.bail_out("Request body too large ({}), connection closed".format(value)) - - self.headers.append((name.decode(), value.decode('utf-8'))) - - def on_headers_complete(self): - self.request = Request( - url_bytes=self.url, - headers=dict(self.headers), - version=self.parser.get_http_version(), - method=self.parser.get_method().decode() - ) - - def on_body(self, body): - self.request.body = body - - def on_message_complete(self): - self.loop.create_task(self.request_handler(self.request, self.write_response)) - - # -------------------------------------------- # - # Responding - # -------------------------------------------- # - - def write_response(self, response): - try: - 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)) - if not keep_alive: - self.transport.close() - else: - self.cleanup() - except Exception as e: - self.bail_out("Writing request failed, connection closed {}".format(e)) - - def bail_out(self, message): - log.error(message) - self.transport.close() - - def cleanup(self): - self.parser = None - self.request = None - self.url = None - self.headers = None - self._total_request_size = 0 - - def close_if_idle(self): - """ - Close the connection if a request is not being sent or received - :return: boolean - True if closed, false if staying open - """ - if not self.parser: - self.transport.close() - return True - return False - - -def serve(host, port, request_handler, after_start=None, before_stop=None, debug=False, request_timeout=60, - request_max_size=None): - # Create Event Loop - loop = async_loop.new_event_loop() - asyncio.set_event_loop(loop) - # I don't think we take advantage of this - # And it slows everything waaayyy down - # loop.set_debug(debug) - - connections = {} - signal = Signal() - server_coroutine = loop.create_server(lambda: HttpProtocol( - loop=loop, - connections=connections, - signal=signal, - request_handler=request_handler, - request_timeout=request_timeout, - request_max_size=request_max_size, - ), host, port) - try: - http_server = loop.run_until_complete(server_coroutine) - except OSError as e: - log.error("Unable to start server: {}".format(e)) - return - except: - log.exception("Unable to start server") - return - - # Run the on_start function if provided - if after_start: - result = after_start(loop) - if isawaitable(result): - loop.run_until_complete(result) - - # Register signals for graceful termination - for _signal in (SIGINT, SIGTERM): - loop.add_signal_handler(_signal, loop.stop) - - try: - loop.run_forever() - finally: - log.info("Stop requested, draining connections...") - - # Run the on_stop function if provided - if before_stop: - result = before_stop(loop) - if isawaitable(result): - loop.run_until_complete(result) - - # Wait for event loop to finish and all connections to drain - http_server.close() - loop.run_until_complete(http_server.wait_closed()) - - # Complete all tasks on the loop - signal.stopped = True - for connection in connections.keys(): - connection.close_if_idle() - - while connections: - loop.run_until_complete(asyncio.sleep(0.1)) - - loop.close() - log.info("Server Stopped") +import asyncio +from inspect import isawaitable +from signal import SIGINT, SIGTERM + +import httptools + +try: + import uvloop as async_loop +except: + async_loop = asyncio + +from .log import log +from .request import Request + + +class Signal: + stopped = False + + +class HttpProtocol(asyncio.Protocol): + __slots__ = ('loop', 'transport', 'connections', 'signal', # event loop, connection + 'parser', 'request', 'url', 'headers', # request params + 'request_handler', 'request_timeout', 'request_max_size', # request config + '_total_request_size', '_timeout_handler') # connection management + + def __init__(self, *, loop, request_handler, signal=Signal(), connections={}, request_timeout=60, + request_max_size=None): + self.loop = loop + self.transport = None + self.request = None + self.parser = None + self.url = None + self.headers = None + self.signal = signal + self.connections = connections + self.request_handler = request_handler + self.request_timeout = request_timeout + self.request_max_size = request_max_size + self._total_request_size = 0 + self._timeout_handler = None + + # -------------------------------------------- # + + # Connection + # -------------------------------------------- # + + def connection_made(self, transport): + self.connections[self] = True + self._timeout_handler = self.loop.call_later(self.request_timeout, self.connection_timeout) + self.transport = transport + + def connection_lost(self, exc): + del self.connections[self] + self._timeout_handler.cancel() + self.cleanup() + + def connection_timeout(self): + self.bail_out("Request timed out, connection closed") + + # -------------------------------------------- # + + # Parsing + # -------------------------------------------- # + + def data_received(self, data): + # Check for the request itself getting too large and exceeding memory limits + self._total_request_size += len(data) + if self._total_request_size > self.request_max_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 + if self.parser is None: + assert self.request is None + self.headers = [] + self.parser = httptools.HttpRequestParser(self) + + # Parse request chunk or close connection + try: + self.parser.feed_data(data) + except httptools.parser.errors.HttpParserError as e: + self.bail_out("Invalid request data, connection closed ({})".format(e)) + + def on_url(self, url): + self.url = url + + def on_header(self, name, value): + if name == b'Content-Length' and int(value) > self.request_max_size: + return self.bail_out("Request body too large ({}), connection closed".format(value)) + + self.headers.append((name.decode(), value.decode('utf-8'))) + + def on_headers_complete(self): + self.request = Request( + url_bytes=self.url, + headers=dict(self.headers), + version=self.parser.get_http_version(), + method=self.parser.get_method().decode() + ) + + def on_body(self, body): + self.request.body = body + + def on_message_complete(self): + self.loop.create_task(self.request_handler(self.request, self.write_response)) + + # -------------------------------------------- # + # Responding + # -------------------------------------------- # + + def write_response(self, response): + try: + 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)) + if not keep_alive: + self.transport.close() + else: + self.cleanup() + except Exception as e: + self.bail_out("Writing request failed, connection closed {}".format(e)) + + def bail_out(self, message): + log.error(message) + self.transport.close() + + def cleanup(self): + self.parser = None + self.request = None + self.url = None + self.headers = None + self._total_request_size = 0 + + def close_if_idle(self): + """ + Close the connection if a request is not being sent or received + :return: boolean - True if closed, false if staying open + """ + if not self.parser: + self.transport.close() + return True + return False + + +def serve(host, port, request_handler, after_start=None, before_stop=None, debug=False, request_timeout=60, + request_max_size=None): + # Create Event Loop + loop = async_loop.new_event_loop() + asyncio.set_event_loop(loop) + # I don't think we take advantage of this + # And it slows everything waaayyy down + # loop.set_debug(debug) + + connections = {} + signal = Signal() + server_coroutine = loop.create_server(lambda: HttpProtocol( + loop=loop, + connections=connections, + signal=signal, + request_handler=request_handler, + request_timeout=request_timeout, + request_max_size=request_max_size, + ), host, port) + try: + http_server = loop.run_until_complete(server_coroutine) + except OSError as e: + log.error("Unable to start server: {}".format(e)) + return + except: + log.exception("Unable to start server") + return + + # Run the on_start function if provided + if after_start: + result = after_start(loop) + if isawaitable(result): + loop.run_until_complete(result) + + # Register signals for graceful termination + for _signal in (SIGINT, SIGTERM): + loop.add_signal_handler(_signal, loop.stop) + + try: + loop.run_forever() + finally: + log.info("Stop requested, draining connections...") + + # Run the on_stop function if provided + if before_stop: + result = before_stop(loop) + if isawaitable(result): + loop.run_until_complete(result) + + # Wait for event loop to finish and all connections to drain + http_server.close() + loop.run_until_complete(http_server.wait_closed()) + + # Complete all tasks on the loop + signal.stopped = True + for connection in connections.keys(): + connection.close_if_idle() + + while connections: + loop.run_until_complete(asyncio.sleep(0.1)) + + loop.close() + log.info("Server Stopped") diff --git a/tests/performance/sanic/http_response.py b/tests/performance/sanic/http_response.py index 71b1f755..40557ab7 100644 --- a/tests/performance/sanic/http_response.py +++ b/tests/performance/sanic/http_response.py @@ -1,33 +1,33 @@ -import asyncpg -import sys -import os -import inspect - -currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) -sys.path.insert(0, currentdir + '/../../../') - -import timeit - -from sanic.response import json - -print(json({"test": True}).output()) - -print("Running New 100,000 times") -times = 0 -total_time = 0 -for n in range(6): - time = timeit.timeit('json({ "test":True }).output()', setup='from sanic.response import json', number=100000) - print("Took {} seconds".format(time)) - total_time += time - times += 1 -print("Average: {}".format(total_time / times)) - -print("Running Old 100,000 times") -times = 0 -total_time = 0 -for n in range(6): - time = timeit.timeit('json({ "test":True }).output_old()', setup='from sanic.response import json', number=100000) - print("Took {} seconds".format(time)) - total_time += time - times += 1 -print("Average: {}".format(total_time / times)) +import asyncpg +import sys +import os +import inspect + +currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) +sys.path.insert(0, currentdir + '/../../../') + +import timeit + +from sanic.response import json + +print(json({"test": True}).output()) + +print("Running New 100,000 times") +times = 0 +total_time = 0 +for n in range(6): + time = timeit.timeit('json({ "test":True }).output()', setup='from sanic.response import json', number=100000) + print("Took {} seconds".format(time)) + total_time += time + times += 1 +print("Average: {}".format(total_time / times)) + +print("Running Old 100,000 times") +times = 0 +total_time = 0 +for n in range(6): + time = timeit.timeit('json({ "test":True }).output_old()', setup='from sanic.response import json', number=100000) + print("Took {} seconds".format(time)) + total_time += time + times += 1 +print("Average: {}".format(total_time / times)) diff --git a/tests/performance/sanic/simple_server.py b/tests/performance/sanic/simple_server.py index d19ffa54..823b7b82 100644 --- a/tests/performance/sanic/simple_server.py +++ b/tests/performance/sanic/simple_server.py @@ -1,19 +1,19 @@ -import sys -import os -import inspect - -currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) -sys.path.insert(0, currentdir + '/../../../') - -from sanic import Sanic -from sanic.response import json - -app = Sanic("test") - - -@app.route("/") -async def test(request): - return json({"test": True}) - - -app.run(host="0.0.0.0", port=sys.argv[1]) +import sys +import os +import inspect + +currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) +sys.path.insert(0, currentdir + '/../../../') + +from sanic import Sanic +from sanic.response import json + +app = Sanic("test") + + +@app.route("/") +async def test(request): + return json({"test": True}) + + +app.run(host="0.0.0.0", port=sys.argv[1]) diff --git a/tests/performance/sanic/varied_server.py b/tests/performance/sanic/varied_server.py index 2c788387..a83e3044 100644 --- a/tests/performance/sanic/varied_server.py +++ b/tests/performance/sanic/varied_server.py @@ -1,91 +1,91 @@ -import sys -import os -import inspect - -currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) -sys.path.insert(0, currentdir + '/../../../') - -from sanic import Sanic -from sanic.response import json, text -from sanic.exceptions import ServerError - -app = Sanic("test") - - -@app.route("/") -async def test(request): - return json({"test": True}) - - -@app.route("/sync", methods=['GET', 'POST']) -def test(request): - return json({"test": True}) - - -@app.route("/text//") -def rtext(request, name, butt): - return text("yeehaww {} {}".format(name, butt)) - - -@app.route("/exception") -def exception(request): - raise ServerError("yep") - - -@app.route("/exception/async") -async def test(request): - raise ServerError("asunk") - - -@app.route("/post_json") -def post_json(request): - return json({"received": True, "message": request.json}) - - -@app.route("/query_string") -def query_string(request): - return json({"parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string}) - - -import sys - -app.run(host="0.0.0.0", port=sys.argv[1]) - - - -# import asyncio_redis -# import asyncpg -# async def setup(sanic, loop): -# sanic.conn = [] -# sanic.redis = [] -# for x in range(10): -# sanic.conn.append(await asyncpg.connect(user='postgres', password='zomgdev', database='postgres', host='192.168.99.100')) -# for n in range(30): -# connection = await asyncio_redis.Connection.create(host='192.168.99.100', port=6379) -# sanic.redis.append(connection) - - -# c=0 -# @app.route("/postgres") -# async def postgres(request): -# global c -# values = await app.conn[c].fetch('''SELECT * FROM players''') -# c += 1 -# if c == 10: -# c = 0 -# return text("yep") - -# r=0 -# @app.route("/redis") -# async def redis(request): -# global r -# try: -# values = await app.redis[r].get('my_key') -# except asyncio_redis.exceptions.ConnectionLostError: -# app.redis[r] = await asyncio_redis.Connection.create(host='127.0.0.1', port=6379) -# values = await app.redis[r].get('my_key') - -# r += 1 -# if r == 30: -# r = 0 -# return text(values) +import sys +import os +import inspect + +currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))) +sys.path.insert(0, currentdir + '/../../../') + +from sanic import Sanic +from sanic.response import json, text +from sanic.exceptions import ServerError + +app = Sanic("test") + + +@app.route("/") +async def test(request): + return json({"test": True}) + + +@app.route("/sync", methods=['GET', 'POST']) +def test(request): + return json({"test": True}) + + +@app.route("/text//") +def rtext(request, name, butt): + return text("yeehaww {} {}".format(name, butt)) + + +@app.route("/exception") +def exception(request): + raise ServerError("yep") + + +@app.route("/exception/async") +async def test(request): + raise ServerError("asunk") + + +@app.route("/post_json") +def post_json(request): + return json({"received": True, "message": request.json}) + + +@app.route("/query_string") +def query_string(request): + return json({"parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string}) + + +import sys + +app.run(host="0.0.0.0", port=sys.argv[1]) + + + +# import asyncio_redis +# import asyncpg +# async def setup(sanic, loop): +# sanic.conn = [] +# sanic.redis = [] +# for x in range(10): +# sanic.conn.append(await asyncpg.connect(user='postgres', password='zomgdev', database='postgres', host='192.168.99.100')) +# for n in range(30): +# connection = await asyncio_redis.Connection.create(host='192.168.99.100', port=6379) +# sanic.redis.append(connection) + + +# c=0 +# @app.route("/postgres") +# async def postgres(request): +# global c +# values = await app.conn[c].fetch('''SELECT * FROM players''') +# c += 1 +# if c == 10: +# c = 0 +# return text("yep") + +# r=0 +# @app.route("/redis") +# async def redis(request): +# global r +# try: +# values = await app.redis[r].get('my_key') +# except asyncio_redis.exceptions.ConnectionLostError: +# app.redis[r] = await asyncio_redis.Connection.create(host='127.0.0.1', port=6379) +# values = await app.redis[r].get('my_key') + +# r += 1 +# if r == 30: +# r = 0 +# return text(values)