Merge pull request #1 from channelcat/master

Updating the fork
This commit is contained in:
Abhishek 2016-10-17 19:36:13 -04:00 committed by GitHub
commit f95fe4192b
11 changed files with 151 additions and 79 deletions

View File

@ -1,13 +1,13 @@
language: python language: python
python: python:
- '3.5' - '3.5'
install: install:
- pip install -r requirements.txt - pip install -r requirements.txt
- pip install -r requirements-dev.txt - pip install -r requirements-dev.txt
- python setup.py install - python setup.py install
- pip install flake8 - pip install flake8
- pip install pytest - pip install pytest
before_script: flake8 --max-line-length=120 sanic before_script: flake8 sanic
script: py.test -v tests script: py.test -v tests
deploy: deploy:
provider: pypi provider: pypi

View File

@ -56,9 +56,7 @@ In this example, the registered routes in the `app.router` will look like:
``` ```
## Middleware ## Middleware
Using blueprints allows you to also register middleware exclusively for that Using blueprints allows you to also register middleware globally.
blueprint, without interfering with other blueprints or routes registered
directly on the application object.
```python ```python
@bp.middleware @bp.middleware
@ -75,8 +73,7 @@ async def halt_response(request, response):
``` ```
## Exceptions ## Exceptions
Exceptions can also be applied exclusively to blueprints without interfering Exceptions can also be applied exclusively to blueprints globally.
with other blueprints or routes registered on the application object.
```python ```python
@bp.exception(NotFound) @bp.exception(NotFound)

View File

@ -77,7 +77,8 @@ class Blueprint:
""" """
""" """
def register_middleware(middleware): def register_middleware(middleware):
self.record(lambda s: s.add_middleware(middleware, *args, **kwargs)) self.record(
lambda s: s.add_middleware(middleware, *args, **kwargs))
return middleware return middleware
# Detect which way this was called, @middleware or @middleware('AT') # Detect which way this was called, @middleware or @middleware('AT')

View File

@ -44,8 +44,13 @@ class Handler:
def default(self, request, exception): def default(self, request, exception):
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 self.sanic.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)

View File

@ -1,4 +1,5 @@
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

@ -42,7 +42,9 @@ class Request:
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 = None
if url_parsed.query:
self.query_string = url_parsed.query.decode('utf-8')
# Init but do not inhale # Init but do not inhale
self.body = None self.body = None
@ -56,7 +58,7 @@ class Request:
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 Exception:
pass pass
return self.parsed_json return self.parsed_json
@ -66,14 +68,19 @@ class Request:
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': is_url_encoded = (
self.parsed_form = RequestParameters(parse_qs(self.body.decode('utf-8'))) content_type == 'application/x-www-form-urlencoded')
if content_type is None or is_url_encoded:
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
@ -91,7 +98,8 @@ class Request:
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 = {}
@ -128,7 +136,8 @@ def parse_multipart_form(body, boundary):
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:
@ -139,7 +148,8 @@ def parse_multipart_form(body, boundary):
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')

View File

@ -19,7 +19,8 @@ STATUS_CODES = {
class HTTPResponse: class HTTPResponse:
__slots__ = ('body', 'status', 'content_type', 'headers') __slots__ = ('body', 'status', 'content_type', 'headers')
def __init__(self, body=None, status=200, headers=None, content_type='text/plain', body_bytes=b''): def __init__(self, body=None, status=200, headers=None,
content_type='text/plain', body_bytes=b''):
self.content_type = content_type self.content_type = content_type
if body is not None: if body is not None:
@ -43,7 +44,12 @@ class HTTPResponse:
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')) b'%b: %b\r\n' % (name.encode(), value.encode('utf-8'))
for name, value in self.headers.items() for name, value in self.headers.items()
) )
return b'HTTP/%b %d %b\r\nContent-Type: %b\r\nContent-Length: %d\r\nConnection: %b\r\n%b%b\r\n%b' % ( return (b'HTTP/%b %d %b\r\n'
b'Content-Type: %b\r\n'
b'Content-Length: %d\r\n'
b'Connection: %b\r\n'
b'%b%b\r\n'
b'%b') % (
version.encode(), version.encode(),
self.status, self.status,
STATUS_CODES.get(self.status, b'FAIL'), STATUS_CODES.get(self.status, b'FAIL'),
@ -62,8 +68,10 @@ def json(body, status=200, headers=None):
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

@ -14,17 +14,19 @@ class Router:
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
Parameters can also have a type by appending :type to the <parameter>. If no type is provided, function provided Parameters can also have a type by appending :type to
a string is expected. A regular expression can also be passed in as the type the <parameter>. If no type is provided, 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, "[^/]+"),
"int": (int, "\d+"), "int": (int, "\d+"),
"number": (float, "[0-9\\.]+"), "number": (float, "[0-9\\.]+"),
"alpha": (None, "[A-Za-z]+"), "alpha": (None, "[A-Za-z]+"),
@ -37,13 +39,17 @@ class Router:
""" """
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.
:param handler: Request handler function. When executed, it should provide a response object. If none are provided, any method is allowed
: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 = None
if methods:
methods_dict = {method: True for method in methods}
parameters = [] parameters = []
@ -71,12 +77,15 @@ class Router:
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
""" """
@ -89,14 +98,18 @@ class Router:
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 if parameter.cast:
kwargs[parameter.name] = parameter.cast(value)
else:
kwargs[parameter.name] = value
route = _route route = _route
break break
if route: if route:
if route.methods and request.method not in route.methods: if route.methods and request.method not in route.methods:
raise InvalidUsage("Method {} not allowed for URL {}".format(request.method, request.url), raise InvalidUsage(
status_code=405) "Method {} not allowed for URL {}".format(
request.method, request.url), 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))
@ -114,15 +127,20 @@ class SimpleRouter:
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 = None
self.routes[uri] = Route(handler=handler, methods=methods_dict, pattern=uri, parameters=None) if methods:
methods_dict = {method: True for method in methods}
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 request.method not in route.methods: if route.methods and request.method not in route.methods:
raise InvalidUsage("Method {} not allowed for URL {}".format(request.method, request.url), raise InvalidUsage(
status_code=405) "Method {} not allowed for URL {}".format(
request.method, request.url), 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

@ -102,10 +102,12 @@ class Sanic:
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
The HTTP Server only expects a response object, so exception handling must be done here be sent back 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:
@ -125,7 +127,9 @@ class Sanic:
# 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)
@ -149,9 +153,12 @@ class Sanic:
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)
@ -159,15 +166,18 @@ class Sanic:
# 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
On termination, drains connections before closing. signal. 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
:param before_stop: Function to be executed when a stop signal is received before it is respected listening
: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
@ -191,7 +201,9 @@ class Sanic:
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 Exception as e:
log.exception(
'Experienced exception while trying to serve: {}'.format(e))
pass pass
def stop(self): def stop(self):

View File

@ -6,7 +6,7 @@ import httptools
try: try:
import uvloop as async_loop import uvloop as async_loop
except: except ImportError:
async_loop = asyncio async_loop = asyncio
from .log import log from .log import log
@ -18,12 +18,18 @@ class Signal:
class HttpProtocol(asyncio.Protocol): class HttpProtocol(asyncio.Protocol):
__slots__ = ('loop', 'transport', 'connections', 'signal', # event loop, connection __slots__ = (
'parser', 'request', 'url', 'headers', # request params # event loop, connection
'request_handler', 'request_timeout', 'request_max_size', # request config 'loop', 'transport', 'connections', 'signal',
'_total_request_size', '_timeout_handler') # connection management # request params
'parser', 'request', 'url', 'headers',
# request config
'request_handler', 'request_timeout', 'request_max_size',
# connection management
'_total_request_size', '_timeout_handler')
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
@ -46,7 +52,8 @@ class HttpProtocol(asyncio.Protocol):
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):
@ -63,10 +70,13 @@ class HttpProtocol(asyncio.Protocol):
# -------------------------------------------- # # -------------------------------------------- #
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:
@ -78,14 +88,16 @@ class HttpProtocol(asyncio.Protocol):
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')))
@ -101,7 +113,8 @@ class HttpProtocol(asyncio.Protocol):
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
@ -109,14 +122,18 @@ class HttpProtocol(asyncio.Protocol):
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 = all(
self.transport.write(response.output(self.request.version, keep_alive, self.request_timeout)) [self.parser.should_keep_alive(), self.signal.stopped])
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)
@ -140,7 +157,8 @@ class HttpProtocol(asyncio.Protocol):
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()
@ -161,12 +179,9 @@ def serve(host, port, request_handler, after_start=None, before_stop=None, debug
), 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 Exception as e:
log.error("Unable to start server: {}".format(e)) log.error("Unable to start server: {}".format(e))
return return
except:
log.exception("Unable to start server")
return
# Run the on_start function if provided # Run the on_start function if provided
if after_start: if after_start:

View File

@ -39,6 +39,11 @@ def test_dynamic_route_string():
assert response.text == 'OK' assert response.text == 'OK'
assert results[0] == 'test123' assert results[0] == 'test123'
request, response = sanic_endpoint_test(app, uri='/folder/favicon.ico')
assert response.text == 'OK'
assert results[1] == 'favicon.ico'
def test_dynamic_route_int(): def test_dynamic_route_int():
app = Sanic('test_dynamic_route_int') app = Sanic('test_dynamic_route_int')