Added examples and form processing
This commit is contained in:
parent
8fbc6c2c4e
commit
49c499f44d
11
examples/Dockerfile
Normal file
11
examples/Dockerfile
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
FROM python:3.5
|
||||||
|
MAINTAINER Channel Cat <channelcat@gmail.com>
|
||||||
|
|
||||||
|
ADD . /code
|
||||||
|
RUN pip3 install git+https://github.com/channelcat/sanic
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
WORKDIR /code
|
||||||
|
|
||||||
|
CMD ["python", "simple_server.py"]
|
6
examples/docker-compose.yml
Normal file
6
examples/docker-compose.yml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
version: '2'
|
||||||
|
services:
|
||||||
|
sanic:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
10
examples/simple_server.py
Normal file
10
examples/simple_server.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
async def test(request):
|
||||||
|
return json({ "test": True })
|
||||||
|
|
||||||
|
app.run(host="0.0.0.0", port=8000)
|
59
examples/try_everything.py
Normal file
59
examples/try_everything.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.log import log
|
||||||
|
from sanic.response import json, text
|
||||||
|
from sanic.exceptions import ServerError
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
async def test_async(request):
|
||||||
|
return json({ "test": True })
|
||||||
|
|
||||||
|
@app.route("/sync", methods=['GET', 'POST'])
|
||||||
|
def test_sync(request):
|
||||||
|
return json({ "test": True })
|
||||||
|
|
||||||
|
@app.route("/dynamic/<name>/<id:int>")
|
||||||
|
def test_params(request, name, id):
|
||||||
|
return text("yeehaww {} {}".format(name, id))
|
||||||
|
|
||||||
|
@app.route("/exception")
|
||||||
|
def exception(request):
|
||||||
|
raise ServerError("It's dead jim")
|
||||||
|
|
||||||
|
# ----------------------------------------------- #
|
||||||
|
# Exceptions
|
||||||
|
# ----------------------------------------------- #
|
||||||
|
|
||||||
|
@app.exception(ServerError)
|
||||||
|
async def test(request, exception):
|
||||||
|
return json({ "exception": "{}".format(exception), "status": exception.status_code }, status=exception.status_code)
|
||||||
|
|
||||||
|
# ----------------------------------------------- #
|
||||||
|
# Read from request
|
||||||
|
# ----------------------------------------------- #
|
||||||
|
|
||||||
|
@app.route("/json")
|
||||||
|
def post_json(request):
|
||||||
|
return json({ "received": True, "message": request.json })
|
||||||
|
|
||||||
|
@app.route("/form")
|
||||||
|
def post_json(request):
|
||||||
|
return json({ "received": True, "form_data": request.form, "penos": request.form.get('penos') })
|
||||||
|
|
||||||
|
@app.route("/query_string")
|
||||||
|
def query_string(request):
|
||||||
|
return json({ "parsed": True, "args": request.args, "url": request.url, "query_string": request.query_string })
|
||||||
|
|
||||||
|
# ----------------------------------------------- #
|
||||||
|
# Run Server
|
||||||
|
# ----------------------------------------------- #
|
||||||
|
|
||||||
|
def before_start(loop):
|
||||||
|
log.info("OH OH OH OH OHHHHHHHH")
|
||||||
|
def before_stop(loop):
|
||||||
|
log.info("TRIED EVERYTHING")
|
||||||
|
|
||||||
|
app.run(host="0.0.0.0", port=8000, debug=True, before_start=before_start, before_stop=before_stop)
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,10 @@ from .response import text
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
|
|
||||||
class SanicException(Exception):
|
class SanicException(Exception):
|
||||||
pass
|
def __init__(self, message, status_code=None):
|
||||||
|
super().__init__(message)
|
||||||
|
if status_code is not None:
|
||||||
|
self.status_code = status_code
|
||||||
|
|
||||||
class NotFound(SanicException):
|
class NotFound(SanicException):
|
||||||
status_code = 404
|
status_code = 404
|
||||||
|
@ -13,26 +16,28 @@ class ServerError(SanicException):
|
||||||
|
|
||||||
class Handler:
|
class Handler:
|
||||||
handlers = None
|
handlers = None
|
||||||
debug = False
|
def __init__(self, sanic):
|
||||||
def __init__(self):
|
|
||||||
self.handlers = {}
|
self.handlers = {}
|
||||||
|
self.sanic = sanic
|
||||||
|
|
||||||
def add(self, exception_type, handler):
|
def add(self, exception, handler):
|
||||||
self.handlers[exception_type] = handler
|
self.handlers[exception] = handler
|
||||||
|
|
||||||
def response(self, request, exception):
|
def response(self, request, exception):
|
||||||
handler = self.handlers.get(type(exception))
|
"""
|
||||||
if handler:
|
Fetches and executes an exception handler and returns a reponse object
|
||||||
response = handler(request, exception)
|
:param request: Request
|
||||||
else:
|
:param exception: Exception to handle
|
||||||
response = Handler.default(request, exception, self.debug)
|
:return: Response object
|
||||||
|
"""
|
||||||
|
handler = self.handlers.get(type(exception), self.default)
|
||||||
|
response = handler(request=request, exception=exception)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@staticmethod
|
def default(self, request, exception):
|
||||||
def default(request, exception, debug):
|
|
||||||
if issubclass(type(exception), SanicException):
|
if issubclass(type(exception), SanicException):
|
||||||
return text("Error: {}".format(exception), status=getattr(exception, 'status_code', 500))
|
return text("Error: {}".format(exception), status=getattr(exception, 'status_code', 500))
|
||||||
elif debug:
|
elif self.sanic.debug:
|
||||||
return text("Error: {}\nException: {}".format(exception, format_exc()), status=500)
|
return text("Error: {}\nException: {}".format(exception, format_exc()), status=500)
|
||||||
else:
|
else:
|
||||||
return text("An error occurred while generating the request", status=500)
|
return text("An error occurred while generating the request", status=500)
|
|
@ -2,8 +2,26 @@ 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
|
||||||
|
|
||||||
|
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:
|
class Request:
|
||||||
__slots__ = ('url', 'headers', 'version', 'method', 'query_string', 'body', 'parsed_json', 'parsed_args')
|
__slots__ = (
|
||||||
|
'url', 'headers', 'version', 'method',
|
||||||
|
'query_string', 'body',
|
||||||
|
'parsed_json', 'parsed_args', 'parsed_form',
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, url_bytes, headers, version, method):
|
def __init__(self, url_bytes, headers, version, method):
|
||||||
# TODO: Content-Encoding detection
|
# TODO: Content-Encoding detection
|
||||||
|
@ -17,27 +35,38 @@ class Request:
|
||||||
# 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_args = None
|
self.parsed_args = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def json(self):
|
def json(self):
|
||||||
if not self.parsed_json:
|
if not self.parsed_json:
|
||||||
if not self.body:
|
try:
|
||||||
raise ValueError("No body to parse")
|
|
||||||
self.parsed_json = json_loads(self.body)
|
self.parsed_json = json_loads(self.body)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
return self.parsed_json
|
return self.parsed_json
|
||||||
|
|
||||||
|
@property
|
||||||
|
def form(self):
|
||||||
|
if not self.parsed_form:
|
||||||
|
content_type = self.headers.get('Content-Type')
|
||||||
|
try:
|
||||||
|
# TODO: form-data
|
||||||
|
if content_type is None or content_type == 'application/x-www-form-urlencoded':
|
||||||
|
self.parsed_form = RequestParameters(parse_qs(self.body.decode('utf-8')))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return self.parsed_form
|
||||||
|
|
||||||
@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:
|
||||||
parsed_query_string = parse_qs(self.query_string).items()
|
self.parsed_args = RequestParameters(parse_qs(self.query_string))
|
||||||
self.parsed_args = {k:[_v for _v in v] if len(v)>1 else v[0] for k,v in parsed_query_string}
|
|
||||||
print(self.parsed_args)
|
|
||||||
else:
|
else:
|
||||||
self.parsed_args = {}
|
self.parsed_args = {}
|
||||||
|
|
||||||
return self.parsed_args
|
return self.parsed_args
|
||||||
|
|
||||||
# TODO: Files
|
|
|
@ -10,7 +10,7 @@ STATUS_CODES = {
|
||||||
402: 'Payment Required',
|
402: 'Payment Required',
|
||||||
403: 'Forbidden',
|
403: 'Forbidden',
|
||||||
404: 'Not Found',
|
404: 'Not Found',
|
||||||
400: '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',
|
||||||
|
@ -19,9 +19,9 @@ STATUS_CODES = {
|
||||||
}
|
}
|
||||||
|
|
||||||
class HTTPResponse:
|
class HTTPResponse:
|
||||||
__slots__ = ('body', 'status', 'content_type')
|
__slots__ = ('body', 'status', 'content_type', 'headers')
|
||||||
|
|
||||||
def __init__(self, body=None, status=200, 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:
|
||||||
|
@ -30,6 +30,7 @@ class HTTPResponse:
|
||||||
self.body = body_bytes
|
self.body = body_bytes
|
||||||
|
|
||||||
self.status = status
|
self.status = status
|
||||||
|
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
|
||||||
|
@ -37,6 +38,9 @@ class HTTPResponse:
|
||||||
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:
|
||||||
|
for name, value in self.headers.items():
|
||||||
|
additional_headers.append('{}: {}\r\n'.format(name, value).encode('utf-8'))
|
||||||
|
|
||||||
return b''.join([
|
return b''.join([
|
||||||
'HTTP/{} {} {}\r\n'.format(version, self.status, STATUS_CODES.get(self.status, 'FAIL')).encode(),
|
'HTTP/{} {} {}\r\n'.format(version, self.status, STATUS_CODES.get(self.status, 'FAIL')).encode(),
|
||||||
|
@ -48,9 +52,9 @@ class HTTPResponse:
|
||||||
self.body,
|
self.body,
|
||||||
])
|
])
|
||||||
|
|
||||||
def json(body, status=200):
|
def json(body, status=200, headers=None):
|
||||||
return HTTPResponse(ujson.dumps(body), status=status, content_type="application/json")
|
return HTTPResponse(ujson.dumps(body), headers=headers, status=status, content_type="application/json")
|
||||||
def text(body, status=200):
|
def text(body, status=200, headers=None):
|
||||||
return HTTPResponse(body, status=status, content_type="text/plain")
|
return HTTPResponse(body, status=status, headers=headers, content_type="text/plain")
|
||||||
def html(body, status=200):
|
def html(body, status=200, headers=None):
|
||||||
return HTTPResponse(body, status=status, content_type="text/html")
|
return HTTPResponse(body, status=status, headers=headers, content_type="text/html")
|
121
sanic/router.py
121
sanic/router.py
|
@ -1,18 +1,123 @@
|
||||||
from .log import log
|
import re
|
||||||
from .exceptions import NotFound
|
from collections import namedtuple
|
||||||
|
from .exceptions import NotFound, InvalidUsage
|
||||||
|
|
||||||
class Router():
|
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/<my_parameter>', 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 <parameter>. 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
|
routes = None
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.routes = {}
|
self.routes = {}
|
||||||
|
|
||||||
def add(self, uri, handler):
|
def add(self, uri, methods, handler):
|
||||||
self.routes[uri] = 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):
|
def get(self, request):
|
||||||
handler = self.routes.get(request.url)
|
route = self.routes.get(request.url)
|
||||||
if handler:
|
if route:
|
||||||
return handler
|
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:
|
else:
|
||||||
raise NotFound("Requested URL {} not found".format(request.url))
|
raise NotFound("Requested URL {} not found".format(request.url))
|
|
@ -6,6 +6,7 @@ from .router import Router
|
||||||
from .server import serve
|
from .server import serve
|
||||||
from .exceptions import ServerError
|
from .exceptions import ServerError
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
|
from traceback import format_exc
|
||||||
|
|
||||||
class Sanic:
|
class Sanic:
|
||||||
name = None
|
name = None
|
||||||
|
@ -17,47 +18,89 @@ 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.error_handler = error_handler or Handler()
|
self.error_handler = error_handler or Handler(self)
|
||||||
self.config = Config()
|
self.config = Config()
|
||||||
|
|
||||||
def route(self, uri):
|
# -------------------------------------------------------------------- #
|
||||||
|
# Decorators
|
||||||
|
# -------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
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):
|
def response(handler):
|
||||||
self.router.add(uri=uri, handler=handler)
|
self.router.add(uri=uri, methods=methods, handler=handler)
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def exception(self, *args, **kwargs):
|
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):
|
def response(handler):
|
||||||
self.error_handler.add(*args, **kwargs)
|
for exception in exceptions:
|
||||||
|
self.error_handler.add(exception, handler)
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
async def handle_request(self, request, respond):
|
# -------------------------------------------------------------------- #
|
||||||
|
# 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:
|
try:
|
||||||
handler = 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")
|
||||||
|
|
||||||
response = handler(request)
|
response = handler(request, *args, **kwargs)
|
||||||
# Check if the handler is asynchronous
|
|
||||||
if isawaitable(response):
|
if isawaitable(response):
|
||||||
response = await response
|
response = await response
|
||||||
|
|
||||||
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):
|
||||||
|
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")
|
||||||
|
|
||||||
respond(response)
|
response_callback(response)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------- #
|
||||||
|
# Execution
|
||||||
|
# -------------------------------------------------------------------- #
|
||||||
|
|
||||||
def run(self, host="127.0.0.1", port=8000, debug=False, on_start=None, on_stop=None):
|
def run(self, host="127.0.0.1", port=8000, debug=False, before_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 before_start: Function to be executed after the event loop is created and before the server starts
|
||||||
|
: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.error_handler.debug=True
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
|
|
||||||
|
@ -68,13 +111,16 @@ class Sanic:
|
||||||
# Serve
|
# Serve
|
||||||
log.info('Goin\' Fast @ {}:{}'.format(host, port))
|
log.info('Goin\' Fast @ {}:{}'.format(host, port))
|
||||||
|
|
||||||
return serve(
|
try:
|
||||||
|
serve(
|
||||||
host=host,
|
host=host,
|
||||||
port=port,
|
port=port,
|
||||||
debug=debug,
|
debug=debug,
|
||||||
on_start=on_start,
|
before_start=before_start,
|
||||||
on_stop=on_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:
|
||||||
|
pass
|
|
@ -78,10 +78,10 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
self.url = url
|
self.url = url
|
||||||
|
|
||||||
def on_header(self, name, value):
|
def on_header(self, name, value):
|
||||||
if name == '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, 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(
|
||||||
|
@ -122,15 +122,25 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
self.headers = None
|
self.headers = None
|
||||||
self._total_request_size = 0
|
self._total_request_size = 0
|
||||||
|
|
||||||
def serve(host, port, request_handler, on_start=None, on_stop=None, debug=False, request_timeout=60, request_max_size=None):
|
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, before_start=None, before_stop=None, debug=False, request_timeout=60, 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)
|
||||||
loop.set_debug(debug)
|
loop.set_debug(debug)
|
||||||
|
|
||||||
# Run the on_start function if provided
|
# Run the on_start function if provided
|
||||||
if on_start:
|
if before_start:
|
||||||
result = on_start(loop)
|
result = before_start(loop)
|
||||||
if isawaitable(result):
|
if isawaitable(result):
|
||||||
loop.run_until_complete(result)
|
loop.run_until_complete(result)
|
||||||
|
|
||||||
|
@ -154,8 +164,8 @@ def serve(host, port, request_handler, on_start=None, on_stop=None, debug=False,
|
||||||
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 on_stop:
|
if before_stop:
|
||||||
result = on_stop(loop)
|
result = before_stop(loop)
|
||||||
if isawaitable(result):
|
if isawaitable(result):
|
||||||
loop.run_until_complete(result)
|
loop.run_until_complete(result)
|
||||||
|
|
||||||
|
@ -165,6 +175,9 @@ def serve(host, port, request_handler, on_start=None, on_stop=None, debug=False,
|
||||||
|
|
||||||
# Complete all tasks on the loop
|
# Complete all tasks on the loop
|
||||||
signal.stopped = True
|
signal.stopped = True
|
||||||
|
for connection in connections.keys():
|
||||||
|
connection.close_if_idle()
|
||||||
|
|
||||||
while connections:
|
while connections:
|
||||||
loop.run_until_complete(asyncio.sleep(0.1))
|
loop.run_until_complete(asyncio.sleep(0.1))
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,7 @@ currentdir = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentfram
|
||||||
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
|
||||||
from sanic.exceptions import ServerError
|
|
||||||
|
|
||||||
app = Sanic("test")
|
app = Sanic("test")
|
||||||
|
|
||||||
|
@ -15,71 +14,4 @@ app = Sanic("test")
|
||||||
async def test(request):
|
async def test(request):
|
||||||
return json({ "test": True })
|
return json({ "test": True })
|
||||||
|
|
||||||
@app.route("/sync")
|
app.run(host="0.0.0.0", port=sys.argv[1])
|
||||||
def test(request):
|
|
||||||
return json({ "test": True })
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/text")
|
|
||||||
def rtext(request):
|
|
||||||
return text("yeehaww")
|
|
||||||
|
|
||||||
@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],)#, on_start=setup)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 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)
|
|
83
tests/performance/sanic/varied_server.py
Normal file
83
tests/performance/sanic/varied_server.py
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
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/<name>/<butt:int>")
|
||||||
|
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)
|
Loading…
Reference in New Issue
Block a user