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
|
||||
*.pyc
|
||||
.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.
|
||||
|
||||
| Server | Requests/sec | Avg Latency |
|
||||
| ---------------------------- | ------------:| -----------:|
|
||||
| Sanic (Python 3.5 + uvloop) | 29,128 | 3.40ms |
|
||||
| Falcon (gunicorn + meinheld) | 18,972 | 5.27ms |
|
||||
| Flask (gunicorn + meinheld) | 4,988 | 20.08ms |
|
||||
| Aiohttp (Python 3.5) | 2,187 | 56.60ms |
|
||||
| Server | Implementation | Requests/sec | Avg Latency |
|
||||
| ------- | ------------------- | ------------:| -----------:|
|
||||
| Sanic | Python 3.5 + uvloop | 29,128 | 3.40ms |
|
||||
| Falcon | gunicorn + meinheld | 18,972 | 5.27ms |
|
||||
| Flask | gunicorn + meinheld | 4,988 | 20.08ms |
|
||||
| Aiohttp | Python 3.5 | 2,187 | 56.60ms |
|
||||
|
||||
## Hello World
|
||||
|
||||
|
@ -33,6 +33,21 @@ app.run(host="0.0.0.0", port=8000)
|
|||
## Installation
|
||||
* `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:
|
||||
|
||||
▄▄▄▄▄
|
||||
|
|
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 urllib.parse import parse_qs
|
||||
from ujson import loads as json_loads
|
||||
|
||||
from .log import log
|
||||
|
||||
class RequestParameters(dict):
|
||||
"""
|
||||
Hosts a dict with lists as values where get returns the first
|
||||
|
@ -20,7 +24,7 @@ class Request:
|
|||
__slots__ = (
|
||||
'url', 'headers', 'version', 'method',
|
||||
'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):
|
||||
|
@ -36,6 +40,7 @@ class Request:
|
|||
self.body = None
|
||||
self.parsed_json = None
|
||||
self.parsed_form = None
|
||||
self.parsed_files = None
|
||||
self.parsed_args = None
|
||||
|
||||
@property
|
||||
|
@ -50,17 +55,30 @@ class Request:
|
|||
|
||||
@property
|
||||
def form(self):
|
||||
if not self.parsed_form:
|
||||
content_type = self.headers.get('Content-Type')
|
||||
if self.parsed_form is None:
|
||||
self.parsed_form = {}
|
||||
self.parsed_files = {}
|
||||
content_type, parameters = parse_header(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:
|
||||
elif content_type == 'multipart/form-data':
|
||||
# TODO: Stream this instead of reading to/from memory
|
||||
boundary = parameters['boundary'].encode('utf-8')
|
||||
self.parsed_form, self.parsed_files = parse_multipart_form(self.body, boundary)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
pass
|
||||
|
||||
return self.parsed_form
|
||||
|
||||
@property
|
||||
def files(self):
|
||||
if self.parsed_files is None:
|
||||
_ = self.form # compute form to get files
|
||||
|
||||
return self.parsed_files
|
||||
|
||||
@property
|
||||
def args(self):
|
||||
if self.parsed_args is None:
|
||||
|
@ -70,3 +88,49 @@ class Request:
|
|||
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):
|
||||
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):
|
||||
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):
|
||||
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 .exceptions import Handler
|
||||
from .log import log, logging
|
||||
from .middleware import Middleware
|
||||
from .response import HTTPResponse
|
||||
from .router import Router
|
||||
from .server import serve
|
||||
from .exceptions import ServerError
|
||||
from inspect import isawaitable
|
||||
from traceback import format_exc
|
||||
|
||||
class Sanic:
|
||||
name = None
|
||||
debug = None
|
||||
router = None
|
||||
error_handler = None
|
||||
routes = []
|
||||
|
||||
def __init__(self, name, router=None, error_handler=None):
|
||||
self.name = name
|
||||
self.router = router or Router()
|
||||
self.router = router or Router()
|
||||
self.error_handler = error_handler or Handler(self)
|
||||
self.config = Config()
|
||||
self.request_middleware = []
|
||||
self.response_middleware = []
|
||||
|
||||
# -------------------------------------------------------------------- #
|
||||
# Decorators
|
||||
# Registration
|
||||
# -------------------------------------------------------------------- #
|
||||
|
||||
# Decorator
|
||||
def route(self, uri, methods=None):
|
||||
"""
|
||||
Decorates a function to be registered as a route
|
||||
|
@ -38,6 +40,7 @@ class Sanic:
|
|||
|
||||
return response
|
||||
|
||||
# Decorator
|
||||
def exception(self, *exceptions):
|
||||
"""
|
||||
Decorates a function to be registered as a route
|
||||
|
@ -52,6 +55,34 @@ class Sanic:
|
|||
|
||||
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
|
||||
# -------------------------------------------------------------------- #
|
||||
|
@ -65,13 +96,35 @@ class Sanic:
|
|||
:return: Nothing
|
||||
"""
|
||||
try:
|
||||
handler, args, kwargs = self.router.get(request)
|
||||
if handler is None:
|
||||
raise ServerError("'None' was returned while requesting a handler from the router")
|
||||
# Middleware process_request
|
||||
response = None
|
||||
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)
|
||||
if isawaitable(response):
|
||||
response = await response
|
||||
# No middleware results
|
||||
if response is None:
|
||||
# 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:
|
||||
try:
|
||||
|
@ -90,14 +143,14 @@ class Sanic:
|
|||
# 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.
|
||||
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 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
|
||||
"""
|
||||
|
@ -116,7 +169,7 @@ class Sanic:
|
|||
host=host,
|
||||
port=port,
|
||||
debug=debug,
|
||||
before_start=before_start,
|
||||
after_start=after_start,
|
||||
before_stop=before_stop,
|
||||
request_handler=self.handle_request,
|
||||
request_timeout=self.config.REQUEST_TIMEOUT,
|
||||
|
@ -124,3 +177,9 @@ class Sanic:
|
|||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
This kills the Sanic
|
||||
"""
|
||||
asyncio.get_event_loop().stop()
|
|
@ -1,5 +1,6 @@
|
|||
import asyncio
|
||||
from inspect import isawaitable
|
||||
from signal import SIGINT, SIGTERM
|
||||
|
||||
import httptools
|
||||
try:
|
||||
|
@ -132,17 +133,13 @@ class HttpProtocol(asyncio.Protocol):
|
|||
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):
|
||||
def serve(host, port, request_handler, after_start=None, before_stop=None, debug=False, request_timeout=60, request_max_size=None):
|
||||
# Create Event Loop
|
||||
loop = async_loop.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.set_debug(debug)
|
||||
|
||||
# Run the on_start function if provided
|
||||
if before_start:
|
||||
result = before_start(loop)
|
||||
if isawaitable(result):
|
||||
loop.run_until_complete(result)
|
||||
# I don't think we take advantage of this
|
||||
# And it slows everything waaayyy down
|
||||
#loop.set_debug(debug)
|
||||
|
||||
connections = {}
|
||||
signal = Signal()
|
||||
|
@ -156,10 +153,18 @@ def serve(host, port, request_handler, before_start=None, before_stop=None, debu
|
|||
), host, port)
|
||||
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:
|
||||
loop.run_forever()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
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):
|
||||
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