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
python:
- '3.5'
- '3.5'
install:
- pip install -r requirements.txt
- pip install -r requirements-dev.txt
- python setup.py install
- pip install flake8
- pip install pytest
before_script: flake8 --max-line-length=120 sanic
- pip install -r requirements.txt
- pip install -r requirements-dev.txt
- python setup.py install
- pip install flake8
- pip install pytest
before_script: flake8 sanic
script: py.test -v tests
deploy:
provider: pypi

View File

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

View File

@ -77,7 +77,8 @@ class Blueprint:
"""
"""
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
# Detect which way this was called, @middleware or @middleware('AT')

View File

@ -44,8 +44,13 @@ class Handler:
def default(self, request, exception):
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:
return text("Error: {}\nException: {}".format(exception, format_exc()), status=500)
return text(
"Error: {}\nException: {}".format(
exception, format_exc()), status=500)
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
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__)

View File

@ -42,7 +42,9 @@ class Request:
self.headers = headers
self.version = version
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
self.body = None
@ -56,7 +58,7 @@ class Request:
if not self.parsed_json:
try:
self.parsed_json = json_loads(self.body)
except:
except Exception:
pass
return self.parsed_json
@ -66,14 +68,19 @@ class Request:
if self.parsed_form is None:
self.parsed_form = {}
self.parsed_files = {}
content_type, parameters = parse_header(self.headers.get('Content-Type'))
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')))
is_url_encoded = (
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':
# 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)
self.parsed_form, self.parsed_files = (
parse_multipart_form(self.body, boundary))
except Exception as e:
log.exception(e)
pass
@ -91,7 +98,8 @@ class Request:
def args(self):
if self.parsed_args is None:
if self.query_string:
self.parsed_args = RequestParameters(parse_qs(self.query_string))
self.parsed_args = RequestParameters(
parse_qs(self.query_string))
else:
self.parsed_args = {}
@ -128,7 +136,8 @@ def parse_multipart_form(body, boundary):
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:])
form_header_value, form_parameters = parse_header(
form_line[colon_index + 2:])
if form_header_field == 'Content-Disposition':
if 'filename' in form_parameters:
@ -139,7 +148,8 @@ def parse_multipart_form(body, boundary):
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)
files[field_name] = File(
type=file_type, name=file_name, body=post_data)
else:
fields[field_name] = post_data.decode('utf-8')

View File

@ -19,7 +19,8 @@ STATUS_CODES = {
class HTTPResponse:
__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
if body is not None:
@ -43,7 +44,12 @@ class HTTPResponse:
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8'))
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(),
self.status,
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):
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):
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):
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
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
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+"),
"string": (None, "[^/]+"),
"int": (int, "\d+"),
"number": (float, "[0-9\\.]+"),
"alpha": (None, "[A-Za-z]+"),
@ -37,13 +39,17 @@ class Router:
"""
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.
: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
methods_dict = None
if methods:
methods_dict = {method: True for method in methods}
parameters = []
@ -71,12 +77,15 @@ class Router:
pattern_string = re.sub("<(.+?)>", add_parameter, uri)
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)
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
:return: handler, arguments, keyword arguments
"""
@ -89,14 +98,18 @@ class Router:
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
if parameter.cast:
kwargs[parameter.name] = parameter.cast(value)
else:
kwargs[parameter.name] = value
route = _route
break
if route:
if route.methods and request.method not in route.methods:
raise InvalidUsage("Method {} not allowed for URL {}".format(request.method, request.url),
status_code=405)
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))
@ -114,15 +127,20 @@ class SimpleRouter:
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)
methods_dict = 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):
route = self.routes.get(request.url)
if route:
if route.methods and request.method not in route.methods:
raise InvalidUsage("Method {} not allowed for URL {}".format(request.method, request.url),
status_code=405)
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))

View File

@ -102,10 +102,12 @@ class Sanic:
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
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
:param response_callback: Response function to be called with the
response as the only argument
:return: Nothing
"""
try:
@ -125,7 +127,9 @@ class Sanic:
# 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")
raise ServerError(
("'None' was returned while requesting a "
"handler from the router"))
# Run response handler
response = handler(request, *args, **kwargs)
@ -149,9 +153,12 @@ class Sanic:
response = await response
except Exception as e:
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:
response = HTTPResponse("An error occured while handling an error")
response = HTTPResponse(
"An error occured while handling an error")
response_callback(response)
@ -159,15 +166,18 @@ class Sanic:
# 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.
On termination, drains connections before closing.
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
: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
@ -191,7 +201,9 @@ class Sanic:
request_timeout=self.config.REQUEST_TIMEOUT,
request_max_size=self.config.REQUEST_MAX_SIZE,
)
except:
except Exception as e:
log.exception(
'Experienced exception while trying to serve: {}'.format(e))
pass
def stop(self):

View File

@ -6,7 +6,7 @@ import httptools
try:
import uvloop as async_loop
except:
except ImportError:
async_loop = asyncio
from .log import log
@ -18,12 +18,18 @@ class Signal:
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
__slots__ = (
# event loop, connection
'loop', 'transport', 'connections', 'signal',
# 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):
self.loop = loop
self.transport = None
@ -46,7 +52,8 @@ class HttpProtocol(asyncio.Protocol):
def connection_made(self, transport):
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
def connection_lost(self, exc):
@ -63,10 +70,13 @@ class HttpProtocol(asyncio.Protocol):
# -------------------------------------------- #
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)
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
if self.parser is None:
@ -78,14 +88,16 @@ class HttpProtocol(asyncio.Protocol):
try:
self.parser.feed_data(data)
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):
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))
return self.bail_out(
"Request body too large ({}), connection closed".format(value))
self.headers.append((name.decode(), value.decode('utf-8')))
@ -101,7 +113,8 @@ class HttpProtocol(asyncio.Protocol):
self.request.body = body
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
@ -109,14 +122,18 @@ class HttpProtocol(asyncio.Protocol):
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))
keep_alive = all(
[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:
self.transport.close()
else:
self.cleanup()
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):
log.error(message)
@ -140,7 +157,8 @@ class HttpProtocol(asyncio.Protocol):
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):
# Create 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)
try:
http_server = loop.run_until_complete(server_coroutine)
except OSError as e:
except Exception 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:

View File

@ -39,6 +39,11 @@ def test_dynamic_route_string():
assert response.text == 'OK'
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():
app = Sanic('test_dynamic_route_int')