Added tests and middleware, and improved documentation
This commit is contained in:
parent
8b1b69eadc
commit
a74ab9bd18
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
||||||
settings.py
|
settings.py
|
||||||
*.pyc
|
*.pyc
|
||||||
.idea/*
|
.idea/*
|
||||||
|
.cache/*
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) [year] [fullname]
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
27
README.md
27
README.md
|
@ -8,12 +8,12 @@ On top of being flask-like, sanic supports async request handlers. This means y
|
||||||
|
|
||||||
All tests were run on a AWS medium instance running ubuntu, using 1 process. Each script delivered a small JSON response and was tested with wrk using 100 connections. Pypy was tested for falcon and flask, but did not speed up requests.
|
All tests were run on a AWS medium instance running ubuntu, using 1 process. Each script delivered a small JSON response and was tested with wrk using 100 connections. Pypy was tested for falcon and flask, but did not speed up requests.
|
||||||
|
|
||||||
| Server | Requests/sec | Avg Latency |
|
| Server | Implementation | Requests/sec | Avg Latency |
|
||||||
| ---------------------------- | ------------:| -----------:|
|
| ------- | ------------------- | ------------:| -----------:|
|
||||||
| Sanic (Python 3.5 + uvloop) | 29,128 | 3.40ms |
|
| Sanic | Python 3.5 + uvloop | 29,128 | 3.40ms |
|
||||||
| Falcon (gunicorn + meinheld) | 18,972 | 5.27ms |
|
| Falcon | gunicorn + meinheld | 18,972 | 5.27ms |
|
||||||
| Flask (gunicorn + meinheld) | 4,988 | 20.08ms |
|
| Flask | gunicorn + meinheld | 4,988 | 20.08ms |
|
||||||
| Aiohttp (Python 3.5) | 2,187 | 56.60ms |
|
| Aiohttp | Python 3.5 | 2,187 | 56.60ms |
|
||||||
|
|
||||||
## Hello World
|
## Hello World
|
||||||
|
|
||||||
|
@ -33,6 +33,21 @@ app.run(host="0.0.0.0", port=8000)
|
||||||
## Installation
|
## Installation
|
||||||
* `python -m pip install git+https://github.com/channelcat/sanic/`
|
* `python -m pip install git+https://github.com/channelcat/sanic/`
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
* [Getting started](docs/getting_started.md)
|
||||||
|
* [Routing](docs/routing.md)
|
||||||
|
* [Middleware](docs/routing.md)
|
||||||
|
* [Request Data](docs/request_data.md)
|
||||||
|
* [Exceptions](docs/exceptions.md)
|
||||||
|
* [License](LICENSE)
|
||||||
|
|
||||||
|
## TODO:
|
||||||
|
* Streamed file processing
|
||||||
|
* File output
|
||||||
|
* Examples of integrations with 3rd-party modules
|
||||||
|
* RESTful router
|
||||||
|
* Blueprints?
|
||||||
|
|
||||||
## Final Thoughts:
|
## Final Thoughts:
|
||||||
|
|
||||||
▄▄▄▄▄
|
▄▄▄▄▄
|
||||||
|
|
14
docs/contributing.md
Normal file
14
docs/contributing.md
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
==========================
|
||||||
|
How to contribute to Sanic
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Thank you for your interest!
|
||||||
|
|
||||||
|
Running tests
|
||||||
|
---------------------
|
||||||
|
* `python -m pip install pytest`
|
||||||
|
* `python -m pytest tests`
|
||||||
|
|
||||||
|
Caution
|
||||||
|
=======
|
||||||
|
One of the main goals of Sanic is speed. Code that lowers the performance of Sanic without significant gains in usability, security, or features may not be merged.
|
0
docs/exceptions.md
Normal file
0
docs/exceptions.md
Normal file
0
docs/getting_started.md
Normal file
0
docs/getting_started.md
Normal file
0
docs/request_data.md
Normal file
0
docs/request_data.md
Normal file
0
docs/routing.md
Normal file
0
docs/routing.md
Normal file
2
pytest.ini
Normal file
2
pytest.ini
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
[pytest]
|
||||||
|
rootdir = /vagrant/Github/sanic
|
4
sanic/middleware.py
Normal file
4
sanic/middleware.py
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
class Middleware:
|
||||||
|
def __init__(self, process_request=None, process_response=None):
|
||||||
|
self.process_request = process_request
|
||||||
|
self.process_response = process_response
|
|
@ -1,7 +1,11 @@
|
||||||
|
from cgi import parse_header
|
||||||
|
from collections import namedtuple
|
||||||
from httptools import parse_url
|
from httptools import parse_url
|
||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs
|
||||||
from ujson import loads as json_loads
|
from ujson import loads as json_loads
|
||||||
|
|
||||||
|
from .log import log
|
||||||
|
|
||||||
class RequestParameters(dict):
|
class RequestParameters(dict):
|
||||||
"""
|
"""
|
||||||
Hosts a dict with lists as values where get returns the first
|
Hosts a dict with lists as values where get returns the first
|
||||||
|
@ -20,7 +24,7 @@ class Request:
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
'url', 'headers', 'version', 'method',
|
'url', 'headers', 'version', 'method',
|
||||||
'query_string', 'body',
|
'query_string', 'body',
|
||||||
'parsed_json', 'parsed_args', 'parsed_form',
|
'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, url_bytes, headers, version, method):
|
def __init__(self, url_bytes, headers, version, method):
|
||||||
|
@ -36,6 +40,7 @@ class Request:
|
||||||
self.body = None
|
self.body = None
|
||||||
self.parsed_json = None
|
self.parsed_json = None
|
||||||
self.parsed_form = None
|
self.parsed_form = None
|
||||||
|
self.parsed_files = None
|
||||||
self.parsed_args = None
|
self.parsed_args = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -50,17 +55,30 @@ class Request:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def form(self):
|
def form(self):
|
||||||
if not self.parsed_form:
|
if self.parsed_form is None:
|
||||||
content_type = self.headers.get('Content-Type')
|
self.parsed_form = {}
|
||||||
|
self.parsed_files = {}
|
||||||
|
content_type, parameters = parse_header(self.headers.get('Content-Type'))
|
||||||
try:
|
try:
|
||||||
# TODO: form-data
|
|
||||||
if content_type is None or content_type == 'application/x-www-form-urlencoded':
|
if content_type is None or content_type == 'application/x-www-form-urlencoded':
|
||||||
self.parsed_form = RequestParameters(parse_qs(self.body.decode('utf-8')))
|
self.parsed_form = RequestParameters(parse_qs(self.body.decode('utf-8')))
|
||||||
except:
|
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
|
pass
|
||||||
|
|
||||||
return self.parsed_form
|
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
|
@property
|
||||||
def args(self):
|
def args(self):
|
||||||
if self.parsed_args is None:
|
if self.parsed_args is None:
|
||||||
|
@ -70,3 +88,49 @@ class Request:
|
||||||
self.parsed_args = {}
|
self.parsed_args = {}
|
||||||
|
|
||||||
return 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
|
|
@ -53,8 +53,8 @@ class HTTPResponse:
|
||||||
])
|
])
|
||||||
|
|
||||||
def json(body, status=200, headers=None):
|
def json(body, status=200, headers=None):
|
||||||
return HTTPResponse(ujson.dumps(body), headers=headers, status=status, content_type="application/json")
|
return HTTPResponse(ujson.dumps(body), headers=headers, status=status, content_type="application/json; charset=utf-8")
|
||||||
def text(body, status=200, headers=None):
|
def text(body, status=200, headers=None):
|
||||||
return HTTPResponse(body, status=status, headers=headers, content_type="text/plain")
|
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")
|
return HTTPResponse(body, status=status, headers=headers, content_type="text/html; charset=utf-8")
|
|
@ -1,30 +1,32 @@
|
||||||
|
import asyncio
|
||||||
|
from inspect import isawaitable
|
||||||
|
from traceback import format_exc
|
||||||
|
from types import FunctionType
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .exceptions import Handler
|
from .exceptions import Handler
|
||||||
from .log import log, logging
|
from .log import log, logging
|
||||||
|
from .middleware import Middleware
|
||||||
from .response import HTTPResponse
|
from .response import HTTPResponse
|
||||||
from .router import Router
|
from .router import Router
|
||||||
from .server import serve
|
from .server import serve
|
||||||
from .exceptions import ServerError
|
from .exceptions import ServerError
|
||||||
from inspect import isawaitable
|
|
||||||
from traceback import format_exc
|
|
||||||
|
|
||||||
class Sanic:
|
class Sanic:
|
||||||
name = None
|
|
||||||
debug = None
|
|
||||||
router = None
|
|
||||||
error_handler = None
|
|
||||||
routes = []
|
|
||||||
|
|
||||||
def __init__(self, name, router=None, error_handler=None):
|
def __init__(self, name, router=None, error_handler=None):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.router = router or Router()
|
self.router = router or Router()
|
||||||
|
self.router = router or Router()
|
||||||
self.error_handler = error_handler or Handler(self)
|
self.error_handler = error_handler or Handler(self)
|
||||||
self.config = Config()
|
self.config = Config()
|
||||||
|
self.request_middleware = []
|
||||||
|
self.response_middleware = []
|
||||||
|
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
# Decorators
|
# Registration
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
# Decorator
|
||||||
def route(self, uri, methods=None):
|
def route(self, uri, methods=None):
|
||||||
"""
|
"""
|
||||||
Decorates a function to be registered as a route
|
Decorates a function to be registered as a route
|
||||||
|
@ -38,6 +40,7 @@ class Sanic:
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
# Decorator
|
||||||
def exception(self, *exceptions):
|
def exception(self, *exceptions):
|
||||||
"""
|
"""
|
||||||
Decorates a function to be registered as a route
|
Decorates a function to be registered as a route
|
||||||
|
@ -52,6 +55,34 @@ class Sanic:
|
||||||
|
|
||||||
return response
|
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]
|
||||||
|
log.info(attach_to)
|
||||||
|
return register_middleware
|
||||||
|
|
||||||
|
if isinstance(middleware, FunctionType):
|
||||||
|
middleware = Middleware(process_request=middleware)
|
||||||
|
|
||||||
|
return middleware
|
||||||
|
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
# Request Handling
|
# Request Handling
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
|
@ -65,13 +96,35 @@ class Sanic:
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
handler, args, kwargs = self.router.get(request)
|
# Middleware process_request
|
||||||
if handler is None:
|
response = None
|
||||||
raise ServerError("'None' was returned while requesting a handler from the router")
|
for middleware in self.request_middleware:
|
||||||
|
response = middleware(request)
|
||||||
|
if isawaitable(response):
|
||||||
|
response = await response
|
||||||
|
if response is not None:
|
||||||
|
break
|
||||||
|
|
||||||
response = handler(request, *args, **kwargs)
|
# No middleware results
|
||||||
if isawaitable(response):
|
if response is None:
|
||||||
response = await 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
|
||||||
|
for middleware in self.response_middleware:
|
||||||
|
_response = middleware(request, response)
|
||||||
|
if isawaitable(_response):
|
||||||
|
_response = await _response
|
||||||
|
if _response is not None:
|
||||||
|
response = _response
|
||||||
|
break
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
try:
|
try:
|
||||||
|
@ -90,14 +143,14 @@ class Sanic:
|
||||||
# Execution
|
# Execution
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
|
|
||||||
def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None, before_stop=None):
|
def run(self, host="127.0.0.1", port=8000, debug=False, after_start=None, before_stop=None):
|
||||||
"""
|
"""
|
||||||
Runs the HTTP Server and listens until keyboard interrupt or term signal.
|
Runs the HTTP Server and listens until keyboard interrupt or term signal.
|
||||||
On termination, drains connections before closing.
|
On termination, drains connections before closing.
|
||||||
:param host: Address to host on
|
:param host: Address to host on
|
||||||
:param port: Port to host on
|
:param port: Port to host on
|
||||||
:param debug: Enables debug output (slows server)
|
:param debug: Enables debug output (slows server)
|
||||||
:param before_start: Function to be executed after the event loop is created and before the server starts
|
:param after_start: Function to be executed after the server starts listening
|
||||||
:param before_stop: Function to be executed when a stop signal is received before it is respected
|
:param before_stop: Function to be executed when a stop signal is received before it is respected
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
|
@ -116,11 +169,17 @@ class Sanic:
|
||||||
host=host,
|
host=host,
|
||||||
port=port,
|
port=port,
|
||||||
debug=debug,
|
debug=debug,
|
||||||
before_start=before_start,
|
after_start=after_start,
|
||||||
before_stop=before_stop,
|
before_stop=before_stop,
|
||||||
request_handler=self.handle_request,
|
request_handler=self.handle_request,
|
||||||
request_timeout=self.config.REQUEST_TIMEOUT,
|
request_timeout=self.config.REQUEST_TIMEOUT,
|
||||||
request_max_size=self.config.REQUEST_MAX_SIZE,
|
request_max_size=self.config.REQUEST_MAX_SIZE,
|
||||||
)
|
)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""
|
||||||
|
This kills the Sanic
|
||||||
|
"""
|
||||||
|
asyncio.get_event_loop().stop()
|
|
@ -1,5 +1,6 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
|
from signal import SIGINT, SIGTERM
|
||||||
|
|
||||||
import httptools
|
import httptools
|
||||||
try:
|
try:
|
||||||
|
@ -132,17 +133,13 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def serve(host, port, request_handler, before_start=None, before_stop=None, debug=False, request_timeout=60, request_max_size=None):
|
def serve(host, port, request_handler, after_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)
|
# I don't think we take advantage of this
|
||||||
|
# And it slows everything waaayyy down
|
||||||
# Run the on_start function if provided
|
#loop.set_debug(debug)
|
||||||
if before_start:
|
|
||||||
result = before_start(loop)
|
|
||||||
if isawaitable(result):
|
|
||||||
loop.run_until_complete(result)
|
|
||||||
|
|
||||||
connections = {}
|
connections = {}
|
||||||
signal = Signal()
|
signal = Signal()
|
||||||
|
@ -156,10 +153,18 @@ def serve(host, port, request_handler, before_start=None, before_stop=None, debu
|
||||||
), host, port)
|
), host, port)
|
||||||
http_server = loop.run_until_complete(server_coroutine)
|
http_server = loop.run_until_complete(server_coroutine)
|
||||||
|
|
||||||
|
# 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:
|
try:
|
||||||
loop.run_forever()
|
loop.run_forever()
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
finally:
|
finally:
|
||||||
log.info("Stop requested, draining connections...")
|
log.info("Stop requested, draining connections...")
|
||||||
|
|
||||||
|
|
47
tests/helpers.py
Normal file
47
tests/helpers.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import aiohttp
|
||||||
|
from sanic.log import log
|
||||||
|
|
||||||
|
HOST = '127.0.0.1'
|
||||||
|
PORT = 42101
|
||||||
|
|
||||||
|
async def local_request(method, uri, *args, **kwargs):
|
||||||
|
url = 'http://{host}:{port}{uri}'.format(host=HOST, port=PORT, uri=uri)
|
||||||
|
log.info(url)
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with getattr(session, method)(url, *args, **kwargs) as response:
|
||||||
|
response.text = await response.text()
|
||||||
|
return response
|
||||||
|
|
||||||
|
def sanic_endpoint_test(app, method='get', uri='/', gather_request=True, *request_args, **request_kwargs):
|
||||||
|
results = []
|
||||||
|
exceptions = []
|
||||||
|
|
||||||
|
if gather_request:
|
||||||
|
@app.middleware
|
||||||
|
def _collect_request(request):
|
||||||
|
results.append(request)
|
||||||
|
|
||||||
|
async def _collect_response(loop):
|
||||||
|
try:
|
||||||
|
response = await local_request(method, uri, *request_args, **request_kwargs)
|
||||||
|
results.append(response)
|
||||||
|
except Exception as e:
|
||||||
|
exceptions.append(e)
|
||||||
|
app.stop()
|
||||||
|
|
||||||
|
app.run(host='0.0.0.0', port=42101, debug=True, after_start=_collect_response)
|
||||||
|
|
||||||
|
if exceptions:
|
||||||
|
raise ValueError("Exception during request: {}".format(exceptions))
|
||||||
|
|
||||||
|
if gather_request:
|
||||||
|
try:
|
||||||
|
request, response = results
|
||||||
|
return request, response
|
||||||
|
except:
|
||||||
|
raise ValueError("request and response object expected, got ({})".format(results[0].text))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return results[0]
|
||||||
|
except:
|
||||||
|
raise ValueError("request object expected, got ({})".format(results))
|
|
@ -14,4 +14,8 @@ app = Sanic("test")
|
||||||
async def test(request):
|
async def test(request):
|
||||||
return json({ "test": True })
|
return json({ "test": True })
|
||||||
|
|
||||||
app.run(host="0.0.0.0", port=sys.argv[1])
|
@app.route("/file")
|
||||||
|
async def test(request):
|
||||||
|
return json({ "test": True, "files": request.files, "fields": request.form })
|
||||||
|
|
||||||
|
app.run(host="0.0.0.0", port=sys.argv[1], debug=True)
|
81
tests/test_middleware.py
Normal file
81
tests/test_middleware.py
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
from json import loads as json_loads, dumps as json_dumps
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.request import Request
|
||||||
|
from sanic.response import json, text, HTTPResponse
|
||||||
|
from helpers import sanic_endpoint_test
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ #
|
||||||
|
# GET
|
||||||
|
# ------------------------------------------------------------ #
|
||||||
|
|
||||||
|
def test_middleware_request():
|
||||||
|
app = Sanic('test_middleware_request')
|
||||||
|
|
||||||
|
results = []
|
||||||
|
@app.middleware
|
||||||
|
async def handler(request):
|
||||||
|
results.append(request)
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def handler(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app)
|
||||||
|
|
||||||
|
assert response.text == 'OK'
|
||||||
|
assert type(results[0]) is Request
|
||||||
|
|
||||||
|
def test_middleware_response():
|
||||||
|
app = Sanic('test_middleware_response')
|
||||||
|
|
||||||
|
results = []
|
||||||
|
@app.middleware('request')
|
||||||
|
async def process_response(request):
|
||||||
|
results.append(request)
|
||||||
|
@app.middleware('response')
|
||||||
|
async def process_response(request, response):
|
||||||
|
results.append(request)
|
||||||
|
results.append(response)
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def handler(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app)
|
||||||
|
|
||||||
|
assert response.text == 'OK'
|
||||||
|
assert type(results[0]) is Request
|
||||||
|
assert type(results[1]) is Request
|
||||||
|
assert issubclass(type(results[2]), HTTPResponse)
|
||||||
|
|
||||||
|
def test_middleware_override_request():
|
||||||
|
app = Sanic('test_middleware_override_request')
|
||||||
|
|
||||||
|
@app.middleware
|
||||||
|
async def halt_request(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def handler(request):
|
||||||
|
return text('FAIL')
|
||||||
|
|
||||||
|
response = sanic_endpoint_test(app, gather_request=False)
|
||||||
|
|
||||||
|
assert response.status == 200
|
||||||
|
assert response.text == 'OK'
|
||||||
|
|
||||||
|
def test_middleware_override_response():
|
||||||
|
app = Sanic('test_middleware_override_response')
|
||||||
|
|
||||||
|
@app.middleware('response')
|
||||||
|
async def process_response(request, response):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def handler(request):
|
||||||
|
return text('FAIL')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app)
|
||||||
|
|
||||||
|
assert response.status == 200
|
||||||
|
assert response.text == 'OK'
|
79
tests/test_requests.py
Normal file
79
tests/test_requests.py
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
from json import loads as json_loads, dumps as json_dumps
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import json, text
|
||||||
|
from helpers import sanic_endpoint_test
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ #
|
||||||
|
# GET
|
||||||
|
# ------------------------------------------------------------ #
|
||||||
|
|
||||||
|
def test_sync():
|
||||||
|
app = Sanic('test_text')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def handler(request):
|
||||||
|
return text('Hello')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app)
|
||||||
|
|
||||||
|
assert response.text == 'Hello'
|
||||||
|
|
||||||
|
def test_text():
|
||||||
|
app = Sanic('test_text')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def handler(request):
|
||||||
|
return text('Hello')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app)
|
||||||
|
|
||||||
|
assert response.text == 'Hello'
|
||||||
|
|
||||||
|
|
||||||
|
def test_json():
|
||||||
|
app = Sanic('test_json')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def handler(request):
|
||||||
|
return json({"test":True})
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app)
|
||||||
|
|
||||||
|
try:
|
||||||
|
results = json_loads(response.text)
|
||||||
|
except:
|
||||||
|
raise ValueError("Expected JSON response but got '{}'".format(response))
|
||||||
|
|
||||||
|
assert results.get('test') == True
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_string():
|
||||||
|
app = Sanic('test_query_string')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def handler(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, params=[("test1", 1), ("test2", "false"), ("test2", "true")])
|
||||||
|
|
||||||
|
assert request.args.get('test1') == '1'
|
||||||
|
assert request.args.get('test2') == 'false'
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ #
|
||||||
|
# POST
|
||||||
|
# ------------------------------------------------------------ #
|
||||||
|
|
||||||
|
def test_post_json():
|
||||||
|
app = Sanic('test_post_json')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def handler(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
payload = {'test': 'OK'}
|
||||||
|
headers = {'content-type': 'application/json'}
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, data=json_dumps(payload), headers=headers)
|
||||||
|
|
||||||
|
assert request.json.get('test') == 'OK'
|
||||||
|
assert response.text == 'OK'
|
95
tests/test_routes.py
Normal file
95
tests/test_routes.py
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
from json import loads as json_loads, dumps as json_dumps
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import json, text
|
||||||
|
from helpers import sanic_endpoint_test
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ #
|
||||||
|
# UTF-8
|
||||||
|
# ------------------------------------------------------------ #
|
||||||
|
|
||||||
|
def test_dynamic_route():
|
||||||
|
app = Sanic('test_dynamic_route')
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
@app.route('/folder/<name>')
|
||||||
|
async def handler(request, name):
|
||||||
|
results.append(name)
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test123')
|
||||||
|
|
||||||
|
assert response.text == 'OK'
|
||||||
|
assert results[0] == 'test123'
|
||||||
|
|
||||||
|
def test_dynamic_route_string():
|
||||||
|
app = Sanic('test_dynamic_route_string')
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
@app.route('/folder/<name:string>')
|
||||||
|
async def handler(request, name):
|
||||||
|
results.append(name)
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test123')
|
||||||
|
|
||||||
|
assert response.text == 'OK'
|
||||||
|
assert results[0] == 'test123'
|
||||||
|
|
||||||
|
def test_dynamic_route_int():
|
||||||
|
app = Sanic('test_dynamic_route_int')
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
@app.route('/folder/<folder_id:int>')
|
||||||
|
async def handler(request, folder_id):
|
||||||
|
results.append(folder_id)
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/12345')
|
||||||
|
assert response.text == 'OK'
|
||||||
|
assert type(results[0]) is int
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/asdf')
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_dynamic_route_number():
|
||||||
|
app = Sanic('test_dynamic_route_int')
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
@app.route('/weight/<weight:number>')
|
||||||
|
async def handler(request, weight):
|
||||||
|
results.append(weight)
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/weight/12345')
|
||||||
|
assert response.text == 'OK'
|
||||||
|
assert type(results[0]) is float
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/weight/1234.56')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/weight/1234-56')
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
def test_dynamic_route_regex():
|
||||||
|
app = Sanic('test_dynamic_route_int')
|
||||||
|
|
||||||
|
@app.route('/folder/<folder_id:[A-Za-z0-9]{0,4}>')
|
||||||
|
async def handler(request, folder_id):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test1')
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test-123')
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/')
|
||||||
|
assert response.status == 200
|
54
tests/test_utf8.py
Normal file
54
tests/test_utf8.py
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
from json import loads as json_loads, dumps as json_dumps
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import json, text
|
||||||
|
from helpers import sanic_endpoint_test
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ #
|
||||||
|
# UTF-8
|
||||||
|
# ------------------------------------------------------------ #
|
||||||
|
|
||||||
|
def test_utf8_query_string():
|
||||||
|
app = Sanic('test_utf8_query_string')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def handler(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, params=[("utf8", '✓')])
|
||||||
|
assert request.args.get('utf8') == '✓'
|
||||||
|
|
||||||
|
def test_utf8_response():
|
||||||
|
app = Sanic('test_utf8_response')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def handler(request):
|
||||||
|
return text('✓')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app)
|
||||||
|
assert response.text == '✓'
|
||||||
|
|
||||||
|
def skip_test_utf8_route():
|
||||||
|
app = Sanic('skip_test_utf8_route')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def handler(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
# UTF-8 Paths are not supported
|
||||||
|
request, response = sanic_endpoint_test(app, route='/✓', uri='/✓')
|
||||||
|
assert response.text == 'OK'
|
||||||
|
|
||||||
|
def test_utf8_post_json():
|
||||||
|
app = Sanic('test_utf8_post_json')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def handler(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
payload = {'test': '✓'}
|
||||||
|
headers = {'content-type': 'application/json'}
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, data=json_dumps(payload), headers=headers)
|
||||||
|
|
||||||
|
assert request.json.get('test') == '✓'
|
||||||
|
assert response.text == 'OK'
|
Loading…
Reference in New Issue
Block a user