commit
d93da2c1a6
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,11 +1,13 @@
|
||||||
*~
|
*~
|
||||||
*.egg-info
|
*.egg-info
|
||||||
*.egg
|
*.egg
|
||||||
|
*.eggs
|
||||||
|
*.pyc
|
||||||
.coverage
|
.coverage
|
||||||
.coverage.*
|
.coverage.*
|
||||||
coverage
|
coverage
|
||||||
.tox
|
.tox
|
||||||
settings.py
|
settings.py
|
||||||
*.pyc
|
|
||||||
.idea/*
|
.idea/*
|
||||||
.cache/*
|
.cache/*
|
||||||
|
.python-version
|
||||||
|
|
12
.travis.yml
12
.travis.yml
|
@ -1,14 +1,10 @@
|
||||||
|
sudo: false
|
||||||
language: python
|
language: python
|
||||||
python:
|
python:
|
||||||
- '3.5'
|
- '3.5'
|
||||||
install:
|
- '3.6'
|
||||||
- pip install -r requirements.txt
|
install: pip install tox-travis
|
||||||
- pip install -r requirements-dev.txt
|
script: tox
|
||||||
- python setup.py install
|
|
||||||
- pip install flake8
|
|
||||||
- pip install pytest
|
|
||||||
before_script: flake8 sanic
|
|
||||||
script: py.test -v tests
|
|
||||||
deploy:
|
deploy:
|
||||||
provider: pypi
|
provider: pypi
|
||||||
user: channelcat
|
user: channelcat
|
||||||
|
|
17
README.md
17
README.md
|
@ -1,5 +1,7 @@
|
||||||
# Sanic
|
# Sanic
|
||||||
|
|
||||||
|
[![Join the chat at https://gitter.im/sanic-python/Lobby](https://badges.gitter.im/sanic-python/Lobby.svg)](https://gitter.im/sanic-python/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||||
|
|
||||||
[![Build Status](https://travis-ci.org/channelcat/sanic.svg?branch=master)](https://travis-ci.org/channelcat/sanic)
|
[![Build Status](https://travis-ci.org/channelcat/sanic.svg?branch=master)](https://travis-ci.org/channelcat/sanic)
|
||||||
[![PyPI](https://img.shields.io/pypi/v/sanic.svg)](https://pypi.python.org/pypi/sanic/)
|
[![PyPI](https://img.shields.io/pypi/v/sanic.svg)](https://pypi.python.org/pypi/sanic/)
|
||||||
[![PyPI](https://img.shields.io/pypi/pyversions/sanic.svg)](https://pypi.python.org/pypi/sanic/)
|
[![PyPI](https://img.shields.io/pypi/pyversions/sanic.svg)](https://pypi.python.org/pypi/sanic/)
|
||||||
|
@ -31,13 +33,17 @@ All tests were run on an AWS medium instance running ubuntu, using 1 process. E
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import json
|
from sanic.response import json
|
||||||
|
|
||||||
app = Sanic(__name__)
|
|
||||||
|
app = Sanic()
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
async def test(request):
|
async def test(request):
|
||||||
return json({ "hello": "world" })
|
return json({"hello": "world"})
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(host="0.0.0.0", port=8000)
|
||||||
|
|
||||||
app.run(host="0.0.0.0", port=8000)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
@ -50,8 +56,11 @@ app.run(host="0.0.0.0", port=8000)
|
||||||
* [Middleware](docs/middleware.md)
|
* [Middleware](docs/middleware.md)
|
||||||
* [Exceptions](docs/exceptions.md)
|
* [Exceptions](docs/exceptions.md)
|
||||||
* [Blueprints](docs/blueprints.md)
|
* [Blueprints](docs/blueprints.md)
|
||||||
|
* [Class Based Views](docs/class_based_views.md)
|
||||||
* [Cookies](docs/cookies.md)
|
* [Cookies](docs/cookies.md)
|
||||||
* [Static Files](docs/static_files.md)
|
* [Static Files](docs/static_files.md)
|
||||||
|
* [Custom Protocol](docs/custom_protocol.md)
|
||||||
|
* [Testing](docs/testing.md)
|
||||||
* [Deploying](docs/deploying.md)
|
* [Deploying](docs/deploying.md)
|
||||||
* [Contributing](docs/contributing.md)
|
* [Contributing](docs/contributing.md)
|
||||||
* [License](LICENSE)
|
* [License](LICENSE)
|
||||||
|
@ -70,7 +79,7 @@ app.run(host="0.0.0.0", port=8000)
|
||||||
▄▄▄▄▄
|
▄▄▄▄▄
|
||||||
▀▀▀██████▄▄▄ _______________
|
▀▀▀██████▄▄▄ _______________
|
||||||
▄▄▄▄▄ █████████▄ / \
|
▄▄▄▄▄ █████████▄ / \
|
||||||
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
|
▀▀▀▀█████▌ ▀▐▄ ▀▐█ | Gotta go fast! |
|
||||||
▀▀█████▄▄ ▀██████▄██ | _________________/
|
▀▀█████▄▄ ▀██████▄██ | _________________/
|
||||||
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
|
▀▄▄▄▄▄ ▀▀█▄▀█════█▀ |/
|
||||||
▀▀▀▄ ▀▀███ ▀ ▄▄
|
▀▀▀▄ ▀▀███ ▀ ▄▄
|
||||||
|
|
58
docs/class_based_views.md
Normal file
58
docs/class_based_views.md
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
# Class based views
|
||||||
|
|
||||||
|
Sanic has simple class based implementation. You should implement methods(get, post, put, patch, delete) for the class to every HTTP method you want to support. If someone tries to use a method that has not been implemented, there will be 405 response.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
```python
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.views import HTTPMethodView
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
app = Sanic('some_name')
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleView(HTTPMethodView):
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return text('I am get method')
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
return text('I am post method')
|
||||||
|
|
||||||
|
def put(self, request):
|
||||||
|
return text('I am put method')
|
||||||
|
|
||||||
|
def patch(self, request):
|
||||||
|
return text('I am patch method')
|
||||||
|
|
||||||
|
def delete(self, request):
|
||||||
|
return text('I am delete method')
|
||||||
|
|
||||||
|
app.add_route(SimpleView.as_view(), '/')
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
If you need any url params just mention them in method definition:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class NameView(HTTPMethodView):
|
||||||
|
|
||||||
|
def get(self, request, name):
|
||||||
|
return text('Hello {}'.format(name))
|
||||||
|
|
||||||
|
app.add_route(NameView.as_view(), '/<name>')
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to add decorator for class, you could set decorators variable
|
||||||
|
|
||||||
|
```
|
||||||
|
class ViewWithDecorator(HTTPMethodView):
|
||||||
|
decorators = [some_decorator_here]
|
||||||
|
|
||||||
|
def get(self, request, name):
|
||||||
|
return text('Hello I have a decorator')
|
||||||
|
|
||||||
|
app.add_route(ViewWithDecorator.as_view(), '/url')
|
||||||
|
|
||||||
|
```
|
70
docs/custom_protocol.md
Normal file
70
docs/custom_protocol.md
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
# Custom Protocol
|
||||||
|
|
||||||
|
You can change the behavior of protocol by using custom protocol.
|
||||||
|
If you want to use custom protocol, you should put subclass of [protocol class](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes) in the protocol keyword argument of `sanic.run()`. The constructor of custom protocol class gets following keyword arguments from Sanic.
|
||||||
|
|
||||||
|
* loop
|
||||||
|
`loop` is an asyncio compatible event loop.
|
||||||
|
|
||||||
|
* connections
|
||||||
|
`connections` is a `set object` to store protocol objects.
|
||||||
|
When Sanic receives `SIGINT` or `SIGTERM`, Sanic executes `protocol.close_if_idle()` for a `protocol objects` stored in connections.
|
||||||
|
|
||||||
|
* signal
|
||||||
|
`signal` is a `sanic.server.Signal object` with `stopped attribute`.
|
||||||
|
When Sanic receives `SIGINT` or `SIGTERM`, `signal.stopped` becomes `True`.
|
||||||
|
|
||||||
|
* request_handler
|
||||||
|
`request_handler` is a coroutine that takes a `sanic.request.Request` object and a `response callback` as arguments.
|
||||||
|
|
||||||
|
* error_handler
|
||||||
|
`error_handler` is a `sanic.exceptions.Handler` object.
|
||||||
|
|
||||||
|
* request_timeout
|
||||||
|
`request_timeout` is seconds for timeout.
|
||||||
|
|
||||||
|
* request_max_size
|
||||||
|
`request_max_size` is bytes of max request size.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
By default protocol, an error occurs, if the handler does not return an `HTTPResponse object`.
|
||||||
|
In this example, By rewriting `write_response()`, if the handler returns `str`, it will be converted to an `HTTPResponse object`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.server import HttpProtocol
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomHttpProtocol(HttpProtocol):
|
||||||
|
|
||||||
|
def __init__(self, *, loop, request_handler, error_handler,
|
||||||
|
signal, connections, request_timeout, request_max_size):
|
||||||
|
super().__init__(
|
||||||
|
loop=loop, request_handler=request_handler,
|
||||||
|
error_handler=error_handler, signal=signal,
|
||||||
|
connections=connections, request_timeout=request_timeout,
|
||||||
|
request_max_size=request_max_size)
|
||||||
|
|
||||||
|
def write_response(self, response):
|
||||||
|
if isinstance(response, str):
|
||||||
|
response = text(response)
|
||||||
|
self.transport.write(
|
||||||
|
response.output(self.request.version)
|
||||||
|
)
|
||||||
|
self.transport.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def string(request):
|
||||||
|
return 'string'
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/1')
|
||||||
|
async def response(request):
|
||||||
|
return text('response')
|
||||||
|
|
||||||
|
app.run(host='0.0.0.0', port=8000, protocol=CustomHttpProtocol)
|
||||||
|
```
|
|
@ -27,3 +27,23 @@ async def handler(request):
|
||||||
|
|
||||||
app.run(host="0.0.0.0", port=8000)
|
app.run(host="0.0.0.0", port=8000)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Middleware chain
|
||||||
|
|
||||||
|
If you want to apply the middleware as a chain, applying more than one, is so easy. You only have to be aware that you do **not return** any response in your middleware:
|
||||||
|
|
||||||
|
```python
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
@app.middleware('response')
|
||||||
|
async def custom_banner(request, response):
|
||||||
|
response.headers["Server"] = "Fake-Server"
|
||||||
|
|
||||||
|
@app.middleware('response')
|
||||||
|
async def prevent_xss(request, response):
|
||||||
|
response.headers["x-xss-protection"] = "1; mode=block"
|
||||||
|
|
||||||
|
app.run(host="0.0.0.0", port=8000)
|
||||||
|
```
|
||||||
|
|
||||||
|
The above code will apply the two middlewares in order. First the middleware **custom_banner** will change the HTTP Response headers *Server* by *Fake-Server*, and the second middleware **prevent_xss** will add the HTTP Headers for prevent Cross-Site-Scripting (XSS) attacks.
|
||||||
|
|
|
@ -29,4 +29,16 @@ async def person_handler(request, name):
|
||||||
async def folder_handler(request, folder_id):
|
async def folder_handler(request, folder_id):
|
||||||
return text('Folder - {}'.format(folder_id))
|
return text('Folder - {}'.format(folder_id))
|
||||||
|
|
||||||
|
async def handler1(request):
|
||||||
|
return text('OK')
|
||||||
|
app.add_route(handler1, '/test')
|
||||||
|
|
||||||
|
async def handler2(request, name):
|
||||||
|
return text('Folder - {}'.format(name))
|
||||||
|
app.add_route(handler2, '/folder/<name>')
|
||||||
|
|
||||||
|
async def person_handler2(request, name):
|
||||||
|
return text('Person - {}'.format(name))
|
||||||
|
app.add_route(person_handler2, '/person/<name:[A-z]>')
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
51
docs/testing.md
Normal file
51
docs/testing.md
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
# Testing
|
||||||
|
|
||||||
|
Sanic endpoints can be tested locally using the `sanic.utils` module, which
|
||||||
|
depends on the additional [aiohttp](https://aiohttp.readthedocs.io/en/stable/)
|
||||||
|
library. The `sanic_endpoint_test` function runs a local server, issues a
|
||||||
|
configurable request to an endpoint, and returns the result. It takes the
|
||||||
|
following arguments:
|
||||||
|
|
||||||
|
- `app` An instance of a Sanic app.
|
||||||
|
- `method` *(default `'get'`)* A string representing the HTTP method to use.
|
||||||
|
- `uri` *(default `'/'`)* A string representing the endpoint to test.
|
||||||
|
- `gather_request` *(default `True`)* A boolean which determines whether the
|
||||||
|
original request will be returned by the function. If set to `True`, the
|
||||||
|
return value is a tuple of `(request, response)`, if `False` only the
|
||||||
|
response is returned.
|
||||||
|
- `loop` *(default `None`)* The event loop to use.
|
||||||
|
- `debug` *(default `False`)* A boolean which determines whether to run the
|
||||||
|
server in debug mode.
|
||||||
|
|
||||||
|
The function further takes the `*request_args` and `**request_kwargs`, which
|
||||||
|
are passed directly to the aiohttp ClientSession request. For example, to
|
||||||
|
supply data with a GET request, `method` would be `get` and the keyword
|
||||||
|
argument `params={'value', 'key'}` would be supplied. More information about
|
||||||
|
the available arguments to aiohttp can be found
|
||||||
|
[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session).
|
||||||
|
|
||||||
|
Below is a complete example of an endpoint test,
|
||||||
|
using [pytest](http://doc.pytest.org/en/latest/). The test checks that the
|
||||||
|
`/challenge` endpoint responds to a GET request with a supplied challenge
|
||||||
|
string.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pytest
|
||||||
|
import aiohttp
|
||||||
|
from sanic.utils import sanic_endpoint_test
|
||||||
|
|
||||||
|
# Import the Sanic app, usually created with Sanic(__name__)
|
||||||
|
from external_server import app
|
||||||
|
|
||||||
|
def test_endpoint_challenge():
|
||||||
|
# Create the challenge data
|
||||||
|
request_data = {'challenge': 'dummy_challenge'}
|
||||||
|
|
||||||
|
# Send the request to the endpoint, using the default `get` method
|
||||||
|
request, response = sanic_endpoint_test(app,
|
||||||
|
uri='/challenge',
|
||||||
|
params=request_data)
|
||||||
|
|
||||||
|
# Assert that the server responds with the challenge string
|
||||||
|
assert response.text == request_data['challenge']
|
||||||
|
```
|
41
examples/cache_example.py
Normal file
41
examples/cache_example.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
"""
|
||||||
|
Example of caching using aiocache package. To run it you will need a Redis
|
||||||
|
instance running in localhost:6379.
|
||||||
|
|
||||||
|
Running this example you will see that the first call lasts 3 seconds and
|
||||||
|
the rest are instant because the value is retrieved from the Redis.
|
||||||
|
|
||||||
|
If you want more info about the package check
|
||||||
|
https://github.com/argaen/aiocache
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import aiocache
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import json
|
||||||
|
from sanic.log import log
|
||||||
|
from aiocache import cached
|
||||||
|
from aiocache.serializers import JsonSerializer
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
aiocache.settings.set_defaults(
|
||||||
|
cache="aiocache.RedisCache"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@cached(key="my_custom_key", serializer=JsonSerializer())
|
||||||
|
async def expensive_call():
|
||||||
|
log.info("Expensive has been called")
|
||||||
|
await asyncio.sleep(3)
|
||||||
|
return {"test": True}
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
async def test(request):
|
||||||
|
log.info("Received GET /")
|
||||||
|
return json(await expensive_call())
|
||||||
|
|
||||||
|
|
||||||
|
app.run(host="0.0.0.0", port=8000, loop=asyncio.get_event_loop())
|
60
examples/exception_monitoring.py
Normal file
60
examples/exception_monitoring.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
"""
|
||||||
|
Example intercepting uncaught exceptions using Sanic's error handler framework.
|
||||||
|
|
||||||
|
This may be useful for developers wishing to use Sentry, Airbrake, etc.
|
||||||
|
or a custom system to log and monitor unexpected errors in production.
|
||||||
|
|
||||||
|
First we create our own class inheriting from Handler in sanic.exceptions,
|
||||||
|
and pass in an instance of it when we create our Sanic instance. Inside this
|
||||||
|
class' default handler, we can do anything including sending exceptions to
|
||||||
|
an external service.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Imports and code relevant for our CustomHandler class
|
||||||
|
(Ordinarily this would be in a separate file)
|
||||||
|
"""
|
||||||
|
from sanic.response import text
|
||||||
|
from sanic.exceptions import Handler, SanicException
|
||||||
|
|
||||||
|
class CustomHandler(Handler):
|
||||||
|
def default(self, request, exception):
|
||||||
|
# Here, we have access to the exception object
|
||||||
|
# and can do anything with it (log, send to external service, etc)
|
||||||
|
|
||||||
|
# Some exceptions are trivial and built into Sanic (404s, etc)
|
||||||
|
if not issubclass(type(exception), SanicException):
|
||||||
|
print(exception)
|
||||||
|
|
||||||
|
# Then, we must finish handling the exception by returning
|
||||||
|
# our response to the client
|
||||||
|
# For this we can just call the super class' default handler
|
||||||
|
return super.default(self, request, exception)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
This is an ordinary Sanic server, with the exception that we set the
|
||||||
|
server's error_handler to an instance of our CustomHandler
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
handler = CustomHandler(sanic=app)
|
||||||
|
app.error_handler = handler
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
async def test(request):
|
||||||
|
# Here, something occurs which causes an unexpected exception
|
||||||
|
# This exception will flow to our custom handler.
|
||||||
|
x = 1 / 0
|
||||||
|
return json({"test": True})
|
||||||
|
|
||||||
|
|
||||||
|
app.run(host="0.0.0.0", port=8000, debug=True)
|
18
examples/jinja_example.py
Normal file
18
examples/jinja_example.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
## To use this example:
|
||||||
|
# curl -d '{"name": "John Doe"}' localhost:8000
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import html
|
||||||
|
from jinja2 import Template
|
||||||
|
|
||||||
|
template = Template('Hello {{ name }}!')
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def test(request):
|
||||||
|
data = request.json
|
||||||
|
return html(template.render(**data))
|
||||||
|
|
||||||
|
|
||||||
|
app.run(host="0.0.0.0", port=8000)
|
23
examples/override_logging.py
Normal file
23
examples/override_logging.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import text
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging_format = "[%(asctime)s] %(process)d-%(levelname)s "
|
||||||
|
logging_format += "%(module)s::%(funcName)s():l%(lineno)d: "
|
||||||
|
logging_format += "%(message)s"
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
format=logging_format,
|
||||||
|
level=logging.DEBUG
|
||||||
|
)
|
||||||
|
log = logging.getLogger()
|
||||||
|
|
||||||
|
# Set logger to override default basicConfig
|
||||||
|
sanic = Sanic(logger=True)
|
||||||
|
@sanic.route("/")
|
||||||
|
def test(request):
|
||||||
|
log.info("received request; responding with 'hey'")
|
||||||
|
return text("hey")
|
||||||
|
|
||||||
|
sanic.run(host="0.0.0.0", port=8000)
|
21
examples/request_timeout.py
Normal file
21
examples/request_timeout.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
from sanic import Sanic
|
||||||
|
import asyncio
|
||||||
|
from sanic.response import text
|
||||||
|
from sanic.config import Config
|
||||||
|
from sanic.exceptions import RequestTimeout
|
||||||
|
|
||||||
|
Config.REQUEST_TIMEOUT = 1
|
||||||
|
app = Sanic(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def test(request):
|
||||||
|
await asyncio.sleep(3)
|
||||||
|
return text('Hello, world!')
|
||||||
|
|
||||||
|
|
||||||
|
@app.exception(RequestTimeout)
|
||||||
|
def timeout(request, exception):
|
||||||
|
return text('RequestTimeout from error_handler.', 408)
|
||||||
|
|
||||||
|
app.run(host='0.0.0.0', port=8000)
|
65
examples/sanic_aiopg_example.py
Normal file
65
examples/sanic_aiopg_example.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
""" To run this example you need additional aiopg package
|
||||||
|
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import uvloop
|
||||||
|
import aiopg
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
|
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||||
|
|
||||||
|
database_name = os.environ['DATABASE_NAME']
|
||||||
|
database_host = os.environ['DATABASE_HOST']
|
||||||
|
database_user = os.environ['DATABASE_USER']
|
||||||
|
database_password = os.environ['DATABASE_PASSWORD']
|
||||||
|
|
||||||
|
connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user,
|
||||||
|
database_password,
|
||||||
|
database_host,
|
||||||
|
database_name)
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
|
|
||||||
|
async def get_pool():
|
||||||
|
return await aiopg.create_pool(connection)
|
||||||
|
|
||||||
|
app = Sanic(name=__name__)
|
||||||
|
pool = loop.run_until_complete(get_pool())
|
||||||
|
|
||||||
|
|
||||||
|
async def prepare_db():
|
||||||
|
""" Let's create some table and add some data
|
||||||
|
|
||||||
|
"""
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
async with conn.cursor() as cur:
|
||||||
|
await cur.execute('DROP TABLE IF EXISTS sanic_polls')
|
||||||
|
await cur.execute("""CREATE TABLE sanic_polls (
|
||||||
|
id serial primary key,
|
||||||
|
question varchar(50),
|
||||||
|
pub_date timestamp
|
||||||
|
);""")
|
||||||
|
for i in range(0, 100):
|
||||||
|
await cur.execute("""INSERT INTO sanic_polls
|
||||||
|
(id, question, pub_date) VALUES ({}, {}, now())
|
||||||
|
""".format(i, i))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
async def handle(request):
|
||||||
|
async with pool.acquire() as conn:
|
||||||
|
async with conn.cursor() as cur:
|
||||||
|
result = []
|
||||||
|
await cur.execute("SELECT question, pub_date FROM sanic_polls")
|
||||||
|
async for row in cur:
|
||||||
|
result.append({"question": row[0], "pub_date": row[1]})
|
||||||
|
return json({"polls": result})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
loop.run_until_complete(prepare_db())
|
||||||
|
app.run(host='0.0.0.0', port=8000, loop=loop)
|
73
examples/sanic_aiopg_sqlalchemy_example.py
Normal file
73
examples/sanic_aiopg_sqlalchemy_example.py
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
""" To run this example you need additional aiopg package
|
||||||
|
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import asyncio
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
import uvloop
|
||||||
|
from aiopg.sa import create_engine
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
|
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||||
|
|
||||||
|
database_name = os.environ['DATABASE_NAME']
|
||||||
|
database_host = os.environ['DATABASE_HOST']
|
||||||
|
database_user = os.environ['DATABASE_USER']
|
||||||
|
database_password = os.environ['DATABASE_PASSWORD']
|
||||||
|
|
||||||
|
connection = 'postgres://{0}:{1}@{2}/{3}'.format(database_user,
|
||||||
|
database_password,
|
||||||
|
database_host,
|
||||||
|
database_name)
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
|
|
||||||
|
metadata = sa.MetaData()
|
||||||
|
|
||||||
|
polls = sa.Table('sanic_polls', metadata,
|
||||||
|
sa.Column('id', sa.Integer, primary_key=True),
|
||||||
|
sa.Column('question', sa.String(50)),
|
||||||
|
sa.Column("pub_date", sa.DateTime))
|
||||||
|
|
||||||
|
|
||||||
|
async def get_engine():
|
||||||
|
return await create_engine(connection)
|
||||||
|
|
||||||
|
app = Sanic(name=__name__)
|
||||||
|
engine = loop.run_until_complete(get_engine())
|
||||||
|
|
||||||
|
|
||||||
|
async def prepare_db():
|
||||||
|
""" Let's add some data
|
||||||
|
|
||||||
|
"""
|
||||||
|
async with engine.acquire() as conn:
|
||||||
|
await conn.execute('DROP TABLE IF EXISTS sanic_polls')
|
||||||
|
await conn.execute("""CREATE TABLE sanic_polls (
|
||||||
|
id serial primary key,
|
||||||
|
question varchar(50),
|
||||||
|
pub_date timestamp
|
||||||
|
);""")
|
||||||
|
for i in range(0, 100):
|
||||||
|
await conn.execute(
|
||||||
|
polls.insert().values(question=i,
|
||||||
|
pub_date=datetime.datetime.now())
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
async def handle(request):
|
||||||
|
async with engine.acquire() as conn:
|
||||||
|
result = []
|
||||||
|
async for row in conn.execute(polls.select()):
|
||||||
|
result.append({"question": row.question, "pub_date": row.pub_date})
|
||||||
|
return json({"polls": result})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
loop.run_until_complete(prepare_db())
|
||||||
|
app.run(host='0.0.0.0', port=8000, loop=loop)
|
65
examples/sanic_asyncpg_example.py
Normal file
65
examples/sanic_asyncpg_example.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
""" To run this example you need additional asyncpg package
|
||||||
|
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import uvloop
|
||||||
|
from asyncpg import create_pool
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import json
|
||||||
|
|
||||||
|
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
||||||
|
|
||||||
|
DB_CONFIG = {
|
||||||
|
'host': '<host>',
|
||||||
|
'user': '<username>',
|
||||||
|
'password': '<password>',
|
||||||
|
'port': '<port>',
|
||||||
|
'database': '<database>'
|
||||||
|
}
|
||||||
|
|
||||||
|
def jsonify(records):
|
||||||
|
""" Parse asyncpg record response into JSON format
|
||||||
|
|
||||||
|
"""
|
||||||
|
return [{key: value for key, value in
|
||||||
|
zip(r.keys(), r.values())} for r in records]
|
||||||
|
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
|
async def make_pool():
|
||||||
|
return await create_pool(**DB_CONFIG)
|
||||||
|
|
||||||
|
app = Sanic(__name__)
|
||||||
|
pool = loop.run_until_complete(make_pool())
|
||||||
|
|
||||||
|
async def create_db():
|
||||||
|
""" Create some table and add some data
|
||||||
|
|
||||||
|
"""
|
||||||
|
async with pool.acquire() as connection:
|
||||||
|
async with connection.transaction():
|
||||||
|
await connection.execute('DROP TABLE IF EXISTS sanic_post')
|
||||||
|
await connection.execute("""CREATE TABLE sanic_post (
|
||||||
|
id serial primary key,
|
||||||
|
content varchar(50),
|
||||||
|
post_date timestamp
|
||||||
|
);""")
|
||||||
|
for i in range(0, 100):
|
||||||
|
await connection.execute(f"""INSERT INTO sanic_post
|
||||||
|
(id, content, post_date) VALUES ({i}, {i}, now())""")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
async def handler(request):
|
||||||
|
async with pool.acquire() as connection:
|
||||||
|
async with connection.transaction():
|
||||||
|
results = await connection.fetch('SELECT * FROM sanic_post')
|
||||||
|
return json({'posts': jsonify(results)})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
loop.run_until_complete(create_db())
|
||||||
|
app.run(host='0.0.0.0', port=8000, loop=loop)
|
|
@ -64,11 +64,11 @@ def query_string(request):
|
||||||
# Run Server
|
# Run Server
|
||||||
# ----------------------------------------------- #
|
# ----------------------------------------------- #
|
||||||
|
|
||||||
def after_start(loop):
|
def after_start(app, loop):
|
||||||
log.info("OH OH OH OH OHHHHHHHH")
|
log.info("OH OH OH OH OHHHHHHHH")
|
||||||
|
|
||||||
|
|
||||||
def before_stop(loop):
|
def before_stop(app, loop):
|
||||||
log.info("TRIED EVERYTHING")
|
log.info("TRIED EVERYTHING")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ httptools
|
||||||
ujson
|
ujson
|
||||||
uvloop
|
uvloop
|
||||||
aiohttp
|
aiohttp
|
||||||
|
aiocache
|
||||||
pytest
|
pytest
|
||||||
coverage
|
coverage
|
||||||
tox
|
tox
|
||||||
|
@ -9,4 +10,5 @@ gunicorn
|
||||||
bottle
|
bottle
|
||||||
kyoukai
|
kyoukai
|
||||||
falcon
|
falcon
|
||||||
tornado
|
tornado
|
||||||
|
aiofiles
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
httptools
|
httptools
|
||||||
ujson
|
ujson
|
||||||
uvloop
|
uvloop
|
||||||
|
aiofiles
|
||||||
|
multidict
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from .sanic import Sanic
|
from .sanic import Sanic
|
||||||
from .blueprints import Blueprint
|
from .blueprints import Blueprint
|
||||||
|
|
||||||
__version__ = '0.1.7'
|
__version__ = '0.1.9'
|
||||||
|
|
||||||
__all__ = ['Sanic', 'Blueprint']
|
__all__ = ['Sanic', 'Blueprint']
|
||||||
|
|
|
@ -20,7 +20,7 @@ if __name__ == "__main__":
|
||||||
|
|
||||||
module = import_module(module_name)
|
module = import_module(module_name)
|
||||||
app = getattr(module, app_name, None)
|
app = getattr(module, app_name, None)
|
||||||
if type(app) is not Sanic:
|
if not isinstance(app, Sanic):
|
||||||
raise ValueError("Module is not a Sanic app, it is a {}. "
|
raise ValueError("Module is not a Sanic app, it is a {}. "
|
||||||
"Perhaps you meant {}.app?"
|
"Perhaps you meant {}.app?"
|
||||||
.format(type(app).__name__, args.module))
|
.format(type(app).__name__, args.module))
|
||||||
|
|
|
@ -91,6 +91,12 @@ class Blueprint:
|
||||||
return handler
|
return handler
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
def add_route(self, handler, uri, methods=None):
|
||||||
|
"""
|
||||||
|
"""
|
||||||
|
self.record(lambda s: s.add_route(handler, uri, methods))
|
||||||
|
return handler
|
||||||
|
|
||||||
def listener(self, event):
|
def listener(self, event):
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
|
@ -109,8 +115,9 @@ class Blueprint:
|
||||||
|
|
||||||
# Detect which way this was called, @middleware or @middleware('AT')
|
# Detect which way this was called, @middleware or @middleware('AT')
|
||||||
if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
|
if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
|
||||||
|
middleware = args[0]
|
||||||
args = []
|
args = []
|
||||||
return register_middleware(args[0])
|
return register_middleware(middleware)
|
||||||
else:
|
else:
|
||||||
return register_middleware
|
return register_middleware
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ def _quote(str):
|
||||||
else:
|
else:
|
||||||
return '"' + str.translate(_Translator) + '"'
|
return '"' + str.translate(_Translator) + '"'
|
||||||
|
|
||||||
|
|
||||||
_is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch
|
_is_legal_key = re.compile('[%s]+' % re.escape(_LegalChars)).fullmatch
|
||||||
|
|
||||||
# ------------------------------------------------------------ #
|
# ------------------------------------------------------------ #
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from .response import text
|
from .response import text
|
||||||
|
from .log import log
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,6 +31,14 @@ class FileNotFound(NotFound):
|
||||||
self.relative_url = relative_url
|
self.relative_url = relative_url
|
||||||
|
|
||||||
|
|
||||||
|
class RequestTimeout(SanicException):
|
||||||
|
status_code = 408
|
||||||
|
|
||||||
|
|
||||||
|
class PayloadTooLarge(SanicException):
|
||||||
|
status_code = 413
|
||||||
|
|
||||||
|
|
||||||
class Handler:
|
class Handler:
|
||||||
handlers = None
|
handlers = None
|
||||||
|
|
||||||
|
@ -48,18 +57,31 @@ class Handler:
|
||||||
:return: Response object
|
:return: Response object
|
||||||
"""
|
"""
|
||||||
handler = self.handlers.get(type(exception), self.default)
|
handler = self.handlers.get(type(exception), self.default)
|
||||||
response = handler(request=request, exception=exception)
|
try:
|
||||||
|
response = handler(request=request, exception=exception)
|
||||||
|
except:
|
||||||
|
if self.sanic.debug:
|
||||||
|
response_message = (
|
||||||
|
'Exception raised in exception handler "{}" '
|
||||||
|
'for uri: "{}"\n{}').format(
|
||||||
|
handler.__name__, request.url, format_exc())
|
||||||
|
log.error(response_message)
|
||||||
|
return text(response_message, 500)
|
||||||
|
else:
|
||||||
|
return text('An error occurred while handling an error', 500)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def default(self, request, exception):
|
def default(self, request, exception):
|
||||||
if issubclass(type(exception), SanicException):
|
if issubclass(type(exception), SanicException):
|
||||||
return text(
|
return text(
|
||||||
"Error: {}".format(exception),
|
'Error: {}'.format(exception),
|
||||||
status=getattr(exception, 'status_code', 500))
|
status=getattr(exception, 'status_code', 500))
|
||||||
elif self.sanic.debug:
|
elif self.sanic.debug:
|
||||||
return text(
|
response_message = (
|
||||||
"Error: {}\nException: {}".format(
|
'Exception occurred while handling uri: "{}"\n{}'.format(
|
||||||
exception, format_exc()), status=500)
|
request.url, format_exc()))
|
||||||
|
log.error(response_message)
|
||||||
|
return text(response_message, status=500)
|
||||||
else:
|
else:
|
||||||
return text(
|
return text(
|
||||||
"An error occurred while generating the request", status=500)
|
'An error occurred while generating the response', status=500)
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s")
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
|
@ -4,10 +4,17 @@ from http.cookies import SimpleCookie
|
||||||
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 sanic.exceptions import InvalidUsage
|
||||||
|
|
||||||
from .log import log
|
from .log import log
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
|
||||||
|
# HTTP/1.1: https://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
|
||||||
|
# > If the media type remains unknown, the recipient SHOULD treat it
|
||||||
|
# > as type "application/octet-stream"
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
@ -26,7 +33,7 @@ class RequestParameters(dict):
|
||||||
return self.super.get(name, default)
|
return self.super.get(name, default)
|
||||||
|
|
||||||
|
|
||||||
class Request:
|
class Request(dict):
|
||||||
"""
|
"""
|
||||||
Properties of an HTTP request such as URL, headers, etc.
|
Properties of an HTTP request such as URL, headers, etc.
|
||||||
"""
|
"""
|
||||||
|
@ -57,25 +64,35 @@ class Request:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def json(self):
|
def json(self):
|
||||||
if not self.parsed_json:
|
if self.parsed_json is None:
|
||||||
try:
|
try:
|
||||||
self.parsed_json = json_loads(self.body)
|
self.parsed_json = json_loads(self.body)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
raise InvalidUsage("Failed when parsing body as json")
|
||||||
|
|
||||||
return self.parsed_json
|
return self.parsed_json
|
||||||
|
|
||||||
|
@property
|
||||||
|
def token(self):
|
||||||
|
"""
|
||||||
|
Attempts to return the auth header token.
|
||||||
|
:return: token related to request
|
||||||
|
"""
|
||||||
|
auth_header = self.headers.get('Authorization')
|
||||||
|
if auth_header is not None:
|
||||||
|
return auth_header.split()[1]
|
||||||
|
return auth_header
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def form(self):
|
def form(self):
|
||||||
if self.parsed_form is None:
|
if self.parsed_form is None:
|
||||||
self.parsed_form = {}
|
self.parsed_form = RequestParameters()
|
||||||
self.parsed_files = {}
|
self.parsed_files = RequestParameters()
|
||||||
content_type, parameters = parse_header(
|
content_type = self.headers.get(
|
||||||
self.headers.get('Content-Type'))
|
'Content-Type', DEFAULT_HTTP_CONTENT_TYPE)
|
||||||
|
content_type, parameters = parse_header(content_type)
|
||||||
try:
|
try:
|
||||||
is_url_encoded = (
|
if content_type == 'application/x-www-form-urlencoded':
|
||||||
content_type == 'application/x-www-form-urlencoded')
|
|
||||||
if content_type is None or is_url_encoded:
|
|
||||||
self.parsed_form = RequestParameters(
|
self.parsed_form = RequestParameters(
|
||||||
parse_qs(self.body.decode('utf-8')))
|
parse_qs(self.body.decode('utf-8')))
|
||||||
elif content_type == 'multipart/form-data':
|
elif content_type == 'multipart/form-data':
|
||||||
|
@ -83,9 +100,8 @@ class Request:
|
||||||
boundary = parameters['boundary'].encode('utf-8')
|
boundary = parameters['boundary'].encode('utf-8')
|
||||||
self.parsed_form, self.parsed_files = (
|
self.parsed_form, self.parsed_files = (
|
||||||
parse_multipart_form(self.body, boundary))
|
parse_multipart_form(self.body, boundary))
|
||||||
except Exception as e:
|
except Exception:
|
||||||
log.exception(e)
|
log.exception("Failed when parsing form")
|
||||||
pass
|
|
||||||
|
|
||||||
return self.parsed_form
|
return self.parsed_form
|
||||||
|
|
||||||
|
@ -110,9 +126,10 @@ class Request:
|
||||||
@property
|
@property
|
||||||
def cookies(self):
|
def cookies(self):
|
||||||
if self._cookies is None:
|
if self._cookies is None:
|
||||||
if 'Cookie' in self.headers:
|
cookie = self.headers.get('Cookie') or self.headers.get('cookie')
|
||||||
|
if cookie is not None:
|
||||||
cookies = SimpleCookie()
|
cookies = SimpleCookie()
|
||||||
cookies.load(self.headers['Cookie'])
|
cookies.load(cookie)
|
||||||
self._cookies = {name: cookie.value
|
self._cookies = {name: cookie.value
|
||||||
for name, cookie in cookies.items()}
|
for name, cookie in cookies.items()}
|
||||||
else:
|
else:
|
||||||
|
@ -128,10 +145,10 @@ def parse_multipart_form(body, boundary):
|
||||||
Parses a request body and returns fields and files
|
Parses a request body and returns fields and files
|
||||||
:param body: Bytes request body
|
:param body: Bytes request body
|
||||||
:param boundary: Bytes multipart boundary
|
:param boundary: Bytes multipart boundary
|
||||||
:return: fields (dict), files (dict)
|
:return: fields (RequestParameters), files (RequestParameters)
|
||||||
"""
|
"""
|
||||||
files = {}
|
files = RequestParameters()
|
||||||
fields = {}
|
fields = RequestParameters()
|
||||||
|
|
||||||
form_parts = body.split(boundary)
|
form_parts = body.split(boundary)
|
||||||
for form_part in form_parts[1:-1]:
|
for form_part in form_parts[1:-1]:
|
||||||
|
@ -162,9 +179,16 @@ 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(
|
file = File(type=file_type, name=file_name, body=post_data)
|
||||||
type=file_type, name=file_name, body=post_data)
|
if field_name in files:
|
||||||
|
files[field_name].append(file)
|
||||||
|
else:
|
||||||
|
files[field_name] = [file]
|
||||||
else:
|
else:
|
||||||
fields[field_name] = post_data.decode('utf-8')
|
value = post_data.decode('utf-8')
|
||||||
|
if field_name in fields:
|
||||||
|
fields[field_name].append(value)
|
||||||
|
else:
|
||||||
|
fields[field_name] = [value]
|
||||||
|
|
||||||
return fields, files
|
return fields, files
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
from aiofiles import open as open_async
|
from aiofiles import open as open_async
|
||||||
from .cookies import CookieJar
|
|
||||||
from mimetypes import guess_type
|
from mimetypes import guess_type
|
||||||
from os import path
|
from os import path
|
||||||
|
|
||||||
from ujson import dumps as json_dumps
|
from ujson import dumps as json_dumps
|
||||||
|
|
||||||
|
from .cookies import CookieJar
|
||||||
|
|
||||||
COMMON_STATUS_CODES = {
|
COMMON_STATUS_CODES = {
|
||||||
200: b'OK',
|
200: b'OK',
|
||||||
400: b'Bad Request',
|
400: b'Bad Request',
|
||||||
|
@ -79,7 +81,12 @@ class HTTPResponse:
|
||||||
self.content_type = content_type
|
self.content_type = content_type
|
||||||
|
|
||||||
if body is not None:
|
if body is not None:
|
||||||
self.body = body.encode('utf-8')
|
try:
|
||||||
|
# Try to encode it regularly
|
||||||
|
self.body = body.encode('utf-8')
|
||||||
|
except AttributeError:
|
||||||
|
# Convert it to a str if you can't
|
||||||
|
self.body = str(body).encode('utf-8')
|
||||||
else:
|
else:
|
||||||
self.body = body_bytes
|
self.body = body_bytes
|
||||||
|
|
||||||
|
@ -96,10 +103,14 @@ class HTTPResponse:
|
||||||
|
|
||||||
headers = b''
|
headers = b''
|
||||||
if self.headers:
|
if self.headers:
|
||||||
headers = b''.join(
|
for name, value in self.headers.items():
|
||||||
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8'))
|
try:
|
||||||
for name, value in self.headers.items()
|
headers += (
|
||||||
)
|
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')))
|
||||||
|
except AttributeError:
|
||||||
|
headers += (
|
||||||
|
b'%b: %b\r\n' % (
|
||||||
|
str(name).encode(), str(value).encode('utf-8')))
|
||||||
|
|
||||||
# Try to pull from the common codes first
|
# Try to pull from the common codes first
|
||||||
# Speeds up response rate 6% over pulling from all
|
# Speeds up response rate 6% over pulling from all
|
||||||
|
|
|
@ -23,18 +23,28 @@ class RouteExists(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RouteDoesNotExist(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Router:
|
class Router:
|
||||||
"""
|
"""
|
||||||
Router supports basic routing with parameters and method checks
|
Router supports basic routing with parameters and method checks
|
||||||
Usage:
|
Usage:
|
||||||
@sanic.route('/my/url/<my_parameter>', methods=['GET', 'POST', ...])
|
@app.route('/my_url/<my_param>', methods=['GET', 'POST', ...])
|
||||||
def my_route(request, my_parameter):
|
def my_route(request, my_param):
|
||||||
|
do stuff...
|
||||||
|
or
|
||||||
|
@app.route('/my_url/<my_param:my_type>', methods=['GET', 'POST', ...])
|
||||||
|
def my_route_with_type(request, my_param: my_type):
|
||||||
do stuff...
|
do stuff...
|
||||||
|
|
||||||
Parameters will be passed as keyword arguments to the request handling
|
Parameters will be passed as keyword arguments to the request handling
|
||||||
function provided Parameters can also have a type by appending :type to
|
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
|
the <parameter>. Given parameter must be able to be type-casted to this.
|
||||||
expression can also be passed in as the type
|
If no type is provided, a string is expected. A regular expression can
|
||||||
|
also be passed in as the type. The argument given to the function will
|
||||||
|
always be a string, independent of the type.
|
||||||
"""
|
"""
|
||||||
routes_static = None
|
routes_static = None
|
||||||
routes_dynamic = None
|
routes_dynamic = None
|
||||||
|
@ -103,6 +113,23 @@ class Router:
|
||||||
else:
|
else:
|
||||||
self.routes_static[uri] = route
|
self.routes_static[uri] = route
|
||||||
|
|
||||||
|
def remove(self, uri, clean_cache=True):
|
||||||
|
try:
|
||||||
|
route = self.routes_all.pop(uri)
|
||||||
|
except KeyError:
|
||||||
|
raise RouteDoesNotExist("Route was not registered: {}".format(uri))
|
||||||
|
|
||||||
|
if route in self.routes_always_check:
|
||||||
|
self.routes_always_check.remove(route)
|
||||||
|
elif url_hash(uri) in self.routes_dynamic \
|
||||||
|
and route in self.routes_dynamic[url_hash(uri)]:
|
||||||
|
self.routes_dynamic[url_hash(uri)].remove(route)
|
||||||
|
else:
|
||||||
|
self.routes_static.pop(uri)
|
||||||
|
|
||||||
|
if clean_cache:
|
||||||
|
self._get.cache_clear()
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
"""
|
"""
|
||||||
Gets a request handler based on the URL of the request, or raises an
|
Gets a request handler based on the URL of the request, or raises an
|
||||||
|
|
122
sanic/sanic.py
122
sanic/sanic.py
|
@ -1,24 +1,35 @@
|
||||||
from asyncio import get_event_loop
|
from asyncio import get_event_loop
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable, stack, getmodulename
|
||||||
from multiprocessing import Process, Event
|
from multiprocessing import Process, Event
|
||||||
from signal import signal, SIGTERM, SIGINT
|
from signal import signal, SIGTERM, SIGINT
|
||||||
from time import sleep
|
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
|
import logging
|
||||||
|
|
||||||
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
|
||||||
from .response import HTTPResponse
|
from .response import HTTPResponse
|
||||||
from .router import Router
|
from .router import Router
|
||||||
from .server import serve
|
from .server import serve, HttpProtocol
|
||||||
from .static import register as static_register
|
from .static import register as static_register
|
||||||
from .exceptions import ServerError
|
from .exceptions import ServerError
|
||||||
|
from socket import socket, SOL_SOCKET, SO_REUSEADDR
|
||||||
|
from os import set_inheritable
|
||||||
|
|
||||||
|
|
||||||
class Sanic:
|
class Sanic:
|
||||||
def __init__(self, name, router=None, error_handler=None):
|
def __init__(self, name=None, router=None,
|
||||||
|
error_handler=None, logger=None):
|
||||||
|
if logger is None:
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s: %(levelname)s: %(message)s"
|
||||||
|
)
|
||||||
|
if name is None:
|
||||||
|
frame_records = stack()[1]
|
||||||
|
name = getmodulename(frame_records[1])
|
||||||
self.name = name
|
self.name = name
|
||||||
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)
|
||||||
|
@ -29,6 +40,8 @@ class Sanic:
|
||||||
self._blueprint_order = []
|
self._blueprint_order = []
|
||||||
self.loop = None
|
self.loop = None
|
||||||
self.debug = None
|
self.debug = None
|
||||||
|
self.sock = None
|
||||||
|
self.processes = None
|
||||||
|
|
||||||
# Register alternative method names
|
# Register alternative method names
|
||||||
self.go_fast = self.run
|
self.go_fast = self.run
|
||||||
|
@ -57,6 +70,22 @@ class Sanic:
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
def add_route(self, handler, uri, methods=None):
|
||||||
|
"""
|
||||||
|
A helper method to register class instance or
|
||||||
|
functions as a handler to the application url
|
||||||
|
routes.
|
||||||
|
:param handler: function or class instance
|
||||||
|
:param uri: path of the URL
|
||||||
|
:param methods: list or tuple of methods allowed
|
||||||
|
:return: function or class instance
|
||||||
|
"""
|
||||||
|
self.route(uri=uri, methods=methods)(handler)
|
||||||
|
return handler
|
||||||
|
|
||||||
|
def remove_route(self, uri, clean_cache=True):
|
||||||
|
self.router.remove(uri, clean_cache)
|
||||||
|
|
||||||
# Decorator
|
# Decorator
|
||||||
def exception(self, *exceptions):
|
def exception(self, *exceptions):
|
||||||
"""
|
"""
|
||||||
|
@ -177,18 +206,18 @@ class Sanic:
|
||||||
if isawaitable(response):
|
if isawaitable(response):
|
||||||
response = await response
|
response = await response
|
||||||
|
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
# Response Middleware
|
# Response Middleware
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
|
|
||||||
if self.response_middleware:
|
if self.response_middleware:
|
||||||
for middleware in self.response_middleware:
|
for middleware in self.response_middleware:
|
||||||
_response = middleware(request, response)
|
_response = middleware(request, response)
|
||||||
if isawaitable(_response):
|
if isawaitable(_response):
|
||||||
_response = await _response
|
_response = await _response
|
||||||
if _response:
|
if _response:
|
||||||
response = _response
|
response = _response
|
||||||
break
|
break
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
|
@ -216,25 +245,27 @@ class Sanic:
|
||||||
|
|
||||||
def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None,
|
def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None,
|
||||||
after_start=None, before_stop=None, after_stop=None, sock=None,
|
after_start=None, before_stop=None, after_stop=None, sock=None,
|
||||||
workers=1, loop=None):
|
workers=1, loop=None, protocol=HttpProtocol, backlog=100,
|
||||||
|
stop_event=None):
|
||||||
"""
|
"""
|
||||||
Runs the HTTP Server and listens until keyboard interrupt or term
|
Runs the HTTP Server and listens until keyboard interrupt or term
|
||||||
signal. 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 before_start: Function to be executed before the server starts
|
:param before_start: Functions to be executed before the server starts
|
||||||
accepting connections
|
accepting connections
|
||||||
:param after_start: Function to be executed after the server starts
|
:param after_start: Functions to be executed after the server starts
|
||||||
accepting connections
|
accepting connections
|
||||||
:param before_stop: Function to be executed when a stop signal is
|
:param before_stop: Functions to be executed when a stop signal is
|
||||||
received before it is respected
|
received before it is respected
|
||||||
:param after_stop: Function to be executed when all requests are
|
:param after_stop: Functions to be executed when all requests are
|
||||||
complete
|
complete
|
||||||
:param sock: Socket for the server to accept connections from
|
:param sock: Socket for the server to accept connections from
|
||||||
:param workers: Number of processes
|
:param workers: Number of processes
|
||||||
received before it is respected
|
received before it is respected
|
||||||
:param loop: asyncio compatible event loop
|
:param loop: asyncio compatible event loop
|
||||||
|
:param protocol: Subclass of asyncio protocol class
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
self.error_handler.debug = True
|
self.error_handler.debug = True
|
||||||
|
@ -242,14 +273,17 @@ class Sanic:
|
||||||
self.loop = loop
|
self.loop = loop
|
||||||
|
|
||||||
server_settings = {
|
server_settings = {
|
||||||
|
'protocol': protocol,
|
||||||
'host': host,
|
'host': host,
|
||||||
'port': port,
|
'port': port,
|
||||||
'sock': sock,
|
'sock': sock,
|
||||||
'debug': debug,
|
'debug': debug,
|
||||||
'request_handler': self.handle_request,
|
'request_handler': self.handle_request,
|
||||||
|
'error_handler': self.error_handler,
|
||||||
'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,
|
||||||
'loop': loop
|
'loop': loop,
|
||||||
|
'backlog': backlog
|
||||||
}
|
}
|
||||||
|
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
|
@ -266,7 +300,7 @@ class Sanic:
|
||||||
for blueprint in self.blueprints.values():
|
for blueprint in self.blueprints.values():
|
||||||
listeners += blueprint.listeners[event_name]
|
listeners += blueprint.listeners[event_name]
|
||||||
if args:
|
if args:
|
||||||
if type(args) is not list:
|
if callable(args):
|
||||||
args = [args]
|
args = [args]
|
||||||
listeners += args
|
listeners += args
|
||||||
if reverse:
|
if reverse:
|
||||||
|
@ -288,12 +322,11 @@ class Sanic:
|
||||||
else:
|
else:
|
||||||
log.info('Spinning up {} workers...'.format(workers))
|
log.info('Spinning up {} workers...'.format(workers))
|
||||||
|
|
||||||
self.serve_multiple(server_settings, workers)
|
self.serve_multiple(server_settings, workers, stop_event)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.exception(
|
log.exception(
|
||||||
'Experienced exception while trying to serve: {}'.format(e))
|
'Experienced exception while trying to serve')
|
||||||
pass
|
|
||||||
|
|
||||||
log.info("Server Stopped")
|
log.info("Server Stopped")
|
||||||
|
|
||||||
|
@ -301,10 +334,13 @@ class Sanic:
|
||||||
"""
|
"""
|
||||||
This kills the Sanic
|
This kills the Sanic
|
||||||
"""
|
"""
|
||||||
|
if self.processes is not None:
|
||||||
|
for process in self.processes:
|
||||||
|
process.terminate()
|
||||||
|
self.sock.close()
|
||||||
get_event_loop().stop()
|
get_event_loop().stop()
|
||||||
|
|
||||||
@staticmethod
|
def serve_multiple(self, server_settings, workers, stop_event=None):
|
||||||
def serve_multiple(server_settings, workers, stop_event=None):
|
|
||||||
"""
|
"""
|
||||||
Starts multiple server processes simultaneously. Stops on interrupt
|
Starts multiple server processes simultaneously. Stops on interrupt
|
||||||
and terminate signals, and drains connections when complete.
|
and terminate signals, and drains connections when complete.
|
||||||
|
@ -316,26 +352,28 @@ class Sanic:
|
||||||
server_settings['reuse_port'] = True
|
server_settings['reuse_port'] = True
|
||||||
|
|
||||||
# Create a stop event to be triggered by a signal
|
# Create a stop event to be triggered by a signal
|
||||||
if not stop_event:
|
if stop_event is None:
|
||||||
stop_event = Event()
|
stop_event = Event()
|
||||||
signal(SIGINT, lambda s, f: stop_event.set())
|
signal(SIGINT, lambda s, f: stop_event.set())
|
||||||
signal(SIGTERM, lambda s, f: stop_event.set())
|
signal(SIGTERM, lambda s, f: stop_event.set())
|
||||||
|
|
||||||
processes = []
|
self.sock = socket()
|
||||||
|
self.sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
|
||||||
|
self.sock.bind((server_settings['host'], server_settings['port']))
|
||||||
|
set_inheritable(self.sock.fileno(), True)
|
||||||
|
server_settings['sock'] = self.sock
|
||||||
|
server_settings['host'] = None
|
||||||
|
server_settings['port'] = None
|
||||||
|
|
||||||
|
self.processes = []
|
||||||
for _ in range(workers):
|
for _ in range(workers):
|
||||||
process = Process(target=serve, kwargs=server_settings)
|
process = Process(target=serve, kwargs=server_settings)
|
||||||
|
process.daemon = True
|
||||||
process.start()
|
process.start()
|
||||||
processes.append(process)
|
self.processes.append(process)
|
||||||
|
|
||||||
# Infinitely wait for the stop event
|
for process in self.processes:
|
||||||
try:
|
|
||||||
while not stop_event.is_set():
|
|
||||||
sleep(0.3)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
log.info('Spinning down workers...')
|
|
||||||
for process in processes:
|
|
||||||
process.terminate()
|
|
||||||
for process in processes:
|
|
||||||
process.join()
|
process.join()
|
||||||
|
|
||||||
|
# the above processes will block this until they're stopped
|
||||||
|
self.stop()
|
||||||
|
|
136
sanic/server.py
136
sanic/server.py
|
@ -1,8 +1,12 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from functools import partial
|
||||||
from inspect import isawaitable
|
from inspect import isawaitable
|
||||||
|
from multidict import CIMultiDict
|
||||||
from signal import SIGINT, SIGTERM
|
from signal import SIGINT, SIGTERM
|
||||||
|
from time import time
|
||||||
import httptools
|
from httptools import HttpRequestParser
|
||||||
|
from httptools.parser.errors import HttpParserError
|
||||||
|
from .exceptions import ServerError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import uvloop as async_loop
|
import uvloop as async_loop
|
||||||
|
@ -11,12 +15,16 @@ except ImportError:
|
||||||
|
|
||||||
from .log import log
|
from .log import log
|
||||||
from .request import Request
|
from .request import Request
|
||||||
|
from .exceptions import RequestTimeout, PayloadTooLarge, InvalidUsage
|
||||||
|
|
||||||
|
|
||||||
class Signal:
|
class Signal:
|
||||||
stopped = False
|
stopped = False
|
||||||
|
|
||||||
|
|
||||||
|
current_time = None
|
||||||
|
|
||||||
|
|
||||||
class HttpProtocol(asyncio.Protocol):
|
class HttpProtocol(asyncio.Protocol):
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
# event loop, connection
|
# event loop, connection
|
||||||
|
@ -26,10 +34,10 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
# request config
|
# request config
|
||||||
'request_handler', 'request_timeout', 'request_max_size',
|
'request_handler', 'request_timeout', 'request_max_size',
|
||||||
# connection management
|
# connection management
|
||||||
'_total_request_size', '_timeout_handler')
|
'_total_request_size', '_timeout_handler', '_last_communication_time')
|
||||||
|
|
||||||
def __init__(self, *, loop, request_handler, signal=Signal(),
|
def __init__(self, *, loop, request_handler, error_handler,
|
||||||
connections={}, request_timeout=60,
|
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
|
||||||
|
@ -40,32 +48,44 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
self.signal = signal
|
self.signal = signal
|
||||||
self.connections = connections
|
self.connections = connections
|
||||||
self.request_handler = request_handler
|
self.request_handler = request_handler
|
||||||
|
self.error_handler = error_handler
|
||||||
self.request_timeout = request_timeout
|
self.request_timeout = request_timeout
|
||||||
self.request_max_size = request_max_size
|
self.request_max_size = request_max_size
|
||||||
self._total_request_size = 0
|
self._total_request_size = 0
|
||||||
self._timeout_handler = None
|
self._timeout_handler = None
|
||||||
|
self._last_request_time = None
|
||||||
|
self._request_handler_task = None
|
||||||
|
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
|
|
||||||
# Connection
|
# Connection
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
|
|
||||||
def connection_made(self, transport):
|
def connection_made(self, transport):
|
||||||
self.connections[self] = True
|
self.connections.add(self)
|
||||||
self._timeout_handler = self.loop.call_later(
|
self._timeout_handler = self.loop.call_later(
|
||||||
self.request_timeout, self.connection_timeout)
|
self.request_timeout, self.connection_timeout)
|
||||||
self.transport = transport
|
self.transport = transport
|
||||||
|
self._last_request_time = current_time
|
||||||
|
|
||||||
def connection_lost(self, exc):
|
def connection_lost(self, exc):
|
||||||
del self.connections[self]
|
self.connections.discard(self)
|
||||||
self._timeout_handler.cancel()
|
self._timeout_handler.cancel()
|
||||||
self.cleanup()
|
self.cleanup()
|
||||||
|
|
||||||
def connection_timeout(self):
|
def connection_timeout(self):
|
||||||
self.bail_out("Request timed out, connection closed")
|
# Check if
|
||||||
|
time_elapsed = current_time - self._last_request_time
|
||||||
# -------------------------------------------- #
|
if time_elapsed < self.request_timeout:
|
||||||
|
time_left = self.request_timeout - time_elapsed
|
||||||
|
self._timeout_handler = \
|
||||||
|
self.loop.call_later(time_left, self.connection_timeout)
|
||||||
|
else:
|
||||||
|
if self._request_handler_task:
|
||||||
|
self._request_handler_task.cancel()
|
||||||
|
exception = RequestTimeout('Request Timeout')
|
||||||
|
self.write_error(exception)
|
||||||
|
|
||||||
|
# -------------------------------------------- #
|
||||||
# Parsing
|
# Parsing
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
|
|
||||||
|
@ -74,37 +94,40 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
# memory limits
|
# 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(
|
exception = PayloadTooLarge('Payload Too Large')
|
||||||
"Request too large ({}), connection closed".format(
|
self.write_error(exception)
|
||||||
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:
|
||||||
assert self.request is None
|
assert self.request is None
|
||||||
self.headers = []
|
self.headers = []
|
||||||
self.parser = httptools.HttpRequestParser(self)
|
self.parser = HttpRequestParser(self)
|
||||||
|
|
||||||
# Parse request chunk or close connection
|
# Parse request chunk or close connection
|
||||||
try:
|
try:
|
||||||
self.parser.feed_data(data)
|
self.parser.feed_data(data)
|
||||||
except httptools.parser.errors.HttpParserError as e:
|
except HttpParserError:
|
||||||
self.bail_out(
|
exception = InvalidUsage('Bad Request')
|
||||||
"Invalid request data, connection closed ({})".format(e))
|
self.write_error(exception)
|
||||||
|
|
||||||
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(
|
exception = PayloadTooLarge('Payload Too Large')
|
||||||
"Request body too large ({}), connection closed".format(value))
|
self.write_error(exception)
|
||||||
|
|
||||||
self.headers.append((name.decode(), value.decode('utf-8')))
|
self.headers.append((name.decode(), value.decode('utf-8')))
|
||||||
|
|
||||||
def on_headers_complete(self):
|
def on_headers_complete(self):
|
||||||
|
remote_addr = self.transport.get_extra_info('peername')
|
||||||
|
if remote_addr:
|
||||||
|
self.headers.append(('Remote-Addr', '%s:%s' % remote_addr))
|
||||||
|
|
||||||
self.request = Request(
|
self.request = Request(
|
||||||
url_bytes=self.url,
|
url_bytes=self.url,
|
||||||
headers=dict(self.headers),
|
headers=CIMultiDict(self.headers),
|
||||||
version=self.parser.get_http_version(),
|
version=self.parser.get_http_version(),
|
||||||
method=self.parser.get_method().decode()
|
method=self.parser.get_method().decode()
|
||||||
)
|
)
|
||||||
|
@ -116,7 +139,7 @@ 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_task = self.loop.create_task(
|
||||||
self.request_handler(self.request, self.write_response))
|
self.request_handler(self.request, self.write_response))
|
||||||
|
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
|
@ -133,20 +156,34 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
if not keep_alive:
|
if not keep_alive:
|
||||||
self.transport.close()
|
self.transport.close()
|
||||||
else:
|
else:
|
||||||
|
# Record that we received data
|
||||||
|
self._last_request_time = current_time
|
||||||
self.cleanup()
|
self.cleanup()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.bail_out(
|
self.bail_out(
|
||||||
"Writing request failed, connection closed {}".format(e))
|
"Writing response failed, connection closed {}".format(e))
|
||||||
|
|
||||||
|
def write_error(self, exception):
|
||||||
|
try:
|
||||||
|
response = self.error_handler.response(self.request, exception)
|
||||||
|
version = self.request.version if self.request else '1.1'
|
||||||
|
self.transport.write(response.output(version))
|
||||||
|
self.transport.close()
|
||||||
|
except Exception as e:
|
||||||
|
self.bail_out(
|
||||||
|
"Writing error failed, connection closed {}".format(e))
|
||||||
|
|
||||||
def bail_out(self, message):
|
def bail_out(self, message):
|
||||||
|
exception = ServerError(message)
|
||||||
|
self.write_error(exception)
|
||||||
log.error(message)
|
log.error(message)
|
||||||
self.transport.close()
|
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
self.parser = None
|
self.parser = None
|
||||||
self.request = None
|
self.request = None
|
||||||
self.url = None
|
self.url = None
|
||||||
self.headers = None
|
self.headers = None
|
||||||
|
self._request_handler_task = None
|
||||||
self._total_request_size = 0
|
self._total_request_size = 0
|
||||||
|
|
||||||
def close_if_idle(self):
|
def close_if_idle(self):
|
||||||
|
@ -160,6 +197,18 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def update_current_time(loop):
|
||||||
|
"""
|
||||||
|
Caches the current time, since it is needed
|
||||||
|
at the end of every keep-alive request to update the request timeout time
|
||||||
|
:param loop:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
global current_time
|
||||||
|
current_time = time()
|
||||||
|
loop.call_later(1, partial(update_current_time, loop))
|
||||||
|
|
||||||
|
|
||||||
def trigger_events(events, loop):
|
def trigger_events(events, loop):
|
||||||
"""
|
"""
|
||||||
:param events: one or more sync or async functions to execute
|
:param events: one or more sync or async functions to execute
|
||||||
|
@ -174,25 +223,31 @@ def trigger_events(events, loop):
|
||||||
loop.run_until_complete(result)
|
loop.run_until_complete(result)
|
||||||
|
|
||||||
|
|
||||||
def serve(host, port, request_handler, before_start=None, after_start=None,
|
def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
before_stop=None, after_stop=None,
|
after_start=None, before_stop=None, after_stop=None, debug=False,
|
||||||
debug=False, request_timeout=60, sock=None,
|
request_timeout=60, sock=None, request_max_size=None,
|
||||||
request_max_size=None, reuse_port=False, loop=None):
|
reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100):
|
||||||
"""
|
"""
|
||||||
Starts asynchronous HTTP Server on an individual process.
|
Starts asynchronous HTTP Server on an individual process.
|
||||||
: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 request_handler: Sanic request handler with middleware
|
:param request_handler: Sanic request handler with middleware
|
||||||
|
:param error_handler: Sanic error handler with middleware
|
||||||
|
:param before_start: Function to be executed before the server starts
|
||||||
|
listening. Takes single argument `loop`
|
||||||
:param after_start: Function to be executed after the server starts
|
:param after_start: Function to be executed after the server starts
|
||||||
listening. Takes single argument `loop`
|
listening. Takes single argument `loop`
|
||||||
:param before_stop: Function to be executed when a stop signal is
|
:param before_stop: Function to be executed when a stop signal is
|
||||||
received before it is respected. Takes single argumenet `loop`
|
received before it is respected. Takes single argumenet `loop`
|
||||||
|
:param after_stop: Function to be executed when a stop signal is
|
||||||
|
received after it is respected. Takes single argumenet `loop`
|
||||||
:param debug: Enables debug output (slows server)
|
:param debug: Enables debug output (slows server)
|
||||||
:param request_timeout: time in seconds
|
:param request_timeout: time in seconds
|
||||||
:param sock: Socket for the server to accept connections from
|
:param sock: Socket for the server to accept connections from
|
||||||
:param request_max_size: size in bytes, `None` for no limit
|
:param request_max_size: size in bytes, `None` for no limit
|
||||||
:param reuse_port: `True` for multiple workers
|
:param reuse_port: `True` for multiple workers
|
||||||
:param loop: asyncio compatible event loop
|
:param loop: asyncio compatible event loop
|
||||||
|
:param protocol: Subclass of asyncio protocol class
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
loop = loop or async_loop.new_event_loop()
|
loop = loop or async_loop.new_event_loop()
|
||||||
|
@ -203,16 +258,31 @@ def serve(host, port, request_handler, before_start=None, after_start=None,
|
||||||
|
|
||||||
trigger_events(before_start, loop)
|
trigger_events(before_start, loop)
|
||||||
|
|
||||||
connections = {}
|
connections = set()
|
||||||
signal = Signal()
|
signal = Signal()
|
||||||
server_coroutine = loop.create_server(lambda: HttpProtocol(
|
server = partial(
|
||||||
|
protocol,
|
||||||
loop=loop,
|
loop=loop,
|
||||||
connections=connections,
|
connections=connections,
|
||||||
signal=signal,
|
signal=signal,
|
||||||
request_handler=request_handler,
|
request_handler=request_handler,
|
||||||
|
error_handler=error_handler,
|
||||||
request_timeout=request_timeout,
|
request_timeout=request_timeout,
|
||||||
request_max_size=request_max_size,
|
request_max_size=request_max_size,
|
||||||
), host, port, reuse_port=reuse_port, sock=sock)
|
)
|
||||||
|
|
||||||
|
server_coroutine = loop.create_server(
|
||||||
|
server,
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
reuse_port=reuse_port,
|
||||||
|
sock=sock,
|
||||||
|
backlog=backlog
|
||||||
|
)
|
||||||
|
|
||||||
|
# Instead of pulling time at the end of every request,
|
||||||
|
# pull it once per minute
|
||||||
|
loop.call_soon(partial(update_current_time, loop))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
http_server = loop.run_until_complete(server_coroutine)
|
http_server = loop.run_until_complete(server_coroutine)
|
||||||
|
@ -240,7 +310,7 @@ def serve(host, port, request_handler, before_start=None, after_start=None,
|
||||||
|
|
||||||
# Complete all tasks on the loop
|
# Complete all tasks on the loop
|
||||||
signal.stopped = True
|
signal.stopped = True
|
||||||
for connection in connections.keys():
|
for connection in connections:
|
||||||
connection.close_if_idle()
|
connection.close_if_idle()
|
||||||
|
|
||||||
while connections:
|
while connections:
|
||||||
|
|
|
@ -2,6 +2,7 @@ from aiofiles.os import stat
|
||||||
from os import path
|
from os import path
|
||||||
from re import sub
|
from re import sub
|
||||||
from time import strftime, gmtime
|
from time import strftime, gmtime
|
||||||
|
from urllib.parse import unquote
|
||||||
|
|
||||||
from .exceptions import FileNotFound, InvalidUsage
|
from .exceptions import FileNotFound, InvalidUsage
|
||||||
from .response import file, HTTPResponse
|
from .response import file, HTTPResponse
|
||||||
|
@ -32,12 +33,17 @@ def register(app, uri, file_or_directory, pattern, use_modified_since):
|
||||||
# served. os.path.realpath seems to be very slow
|
# served. os.path.realpath seems to be very slow
|
||||||
if file_uri and '../' in file_uri:
|
if file_uri and '../' in file_uri:
|
||||||
raise InvalidUsage("Invalid URL")
|
raise InvalidUsage("Invalid URL")
|
||||||
|
|
||||||
# Merge served directory and requested file if provided
|
# Merge served directory and requested file if provided
|
||||||
# Strip all / that in the beginning of the URL to help prevent python
|
# Strip all / that in the beginning of the URL to help prevent python
|
||||||
# from herping a derp and treating the uri as an absolute path
|
# from herping a derp and treating the uri as an absolute path
|
||||||
file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \
|
file_path = file_or_directory
|
||||||
if file_uri else file_or_directory
|
if file_uri:
|
||||||
|
file_path = path.join(
|
||||||
|
file_or_directory, sub('^[/]*', '', file_uri))
|
||||||
|
|
||||||
|
# URL decode the path sent by the browser otherwise we won't be able to
|
||||||
|
# match filenames which got encoded (filenames with spaces etc)
|
||||||
|
file_path = unquote(file_path)
|
||||||
try:
|
try:
|
||||||
headers = {}
|
headers = {}
|
||||||
# Check if the client has been sent this file before
|
# Check if the client has been sent this file before
|
||||||
|
|
|
@ -16,14 +16,15 @@ async def local_request(method, uri, cookies=None, *args, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
|
def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
|
||||||
loop=None, *request_args, **request_kwargs):
|
loop=None, debug=False, server_kwargs={},
|
||||||
|
*request_args, **request_kwargs):
|
||||||
results = []
|
results = []
|
||||||
exceptions = []
|
exceptions = []
|
||||||
|
|
||||||
if gather_request:
|
if gather_request:
|
||||||
@app.middleware
|
|
||||||
def _collect_request(request):
|
def _collect_request(request):
|
||||||
results.append(request)
|
results.append(request)
|
||||||
|
app.request_middleware.appendleft(_collect_request)
|
||||||
|
|
||||||
async def _collect_response(sanic, loop):
|
async def _collect_response(sanic, loop):
|
||||||
try:
|
try:
|
||||||
|
@ -34,7 +35,8 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
|
||||||
exceptions.append(e)
|
exceptions.append(e)
|
||||||
app.stop()
|
app.stop()
|
||||||
|
|
||||||
app.run(host=HOST, port=42101, after_start=_collect_response, loop=loop)
|
app.run(host=HOST, debug=debug, port=PORT,
|
||||||
|
after_start=_collect_response, loop=loop, **server_kwargs)
|
||||||
|
|
||||||
if exceptions:
|
if exceptions:
|
||||||
raise ValueError("Exception during request: {}".format(exceptions))
|
raise ValueError("Exception during request: {}".format(exceptions))
|
||||||
|
@ -45,11 +47,11 @@ def sanic_endpoint_test(app, method='get', uri='/', gather_request=True,
|
||||||
return request, response
|
return request, response
|
||||||
except:
|
except:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"request and response object expected, got ({})".format(
|
"Request and response object expected, got ({})".format(
|
||||||
results))
|
results))
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
return results[0]
|
return results[0]
|
||||||
except:
|
except:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"request object expected, got ({})".format(results))
|
"Request object expected, got ({})".format(results))
|
||||||
|
|
63
sanic/views.py
Normal file
63
sanic/views.py
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
from .exceptions import InvalidUsage
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPMethodView:
|
||||||
|
""" Simple class based implementation of view for the sanic.
|
||||||
|
You should implement methods (get, post, put, patch, delete) for the class
|
||||||
|
to every HTTP method you want to support.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
class DummyView(HTTPMethodView):
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
return text('I am get method')
|
||||||
|
|
||||||
|
def put(self, request, *args, **kwargs):
|
||||||
|
return text('I am put method')
|
||||||
|
etc.
|
||||||
|
|
||||||
|
If someone tries to use a non-implemented method, there will be a
|
||||||
|
405 response.
|
||||||
|
|
||||||
|
If you need any url params just mention them in method definition:
|
||||||
|
class DummyView(HTTPMethodView):
|
||||||
|
|
||||||
|
def get(self, request, my_param_here, *args, **kwargs):
|
||||||
|
return text('I am get method with %s' % my_param_here)
|
||||||
|
|
||||||
|
To add the view into the routing you could use
|
||||||
|
1) app.add_route(DummyView.as_view(), '/')
|
||||||
|
2) app.route('/')(DummyView.as_view())
|
||||||
|
|
||||||
|
To add any decorator you could set it into decorators variable
|
||||||
|
"""
|
||||||
|
|
||||||
|
decorators = []
|
||||||
|
|
||||||
|
def dispatch_request(self, request, *args, **kwargs):
|
||||||
|
handler = getattr(self, request.method.lower(), None)
|
||||||
|
if handler:
|
||||||
|
return handler(request, *args, **kwargs)
|
||||||
|
raise InvalidUsage(
|
||||||
|
'Method {} not allowed for URL {}'.format(
|
||||||
|
request.method, request.url), status_code=405)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def as_view(cls, *class_args, **class_kwargs):
|
||||||
|
""" Converts the class into an actual view function that can be used
|
||||||
|
with the routing system.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def view(*args, **kwargs):
|
||||||
|
self = view.view_class(*class_args, **class_kwargs)
|
||||||
|
return self.dispatch_request(*args, **kwargs)
|
||||||
|
|
||||||
|
if cls.decorators:
|
||||||
|
view.__module__ = cls.__module__
|
||||||
|
for decorator in cls.decorators:
|
||||||
|
view = decorator(view)
|
||||||
|
|
||||||
|
view.view_class = cls
|
||||||
|
view.__doc__ = cls.__doc__
|
||||||
|
view.__module__ = cls.__module__
|
||||||
|
return view
|
1
setup.py
1
setup.py
|
@ -30,6 +30,7 @@ setup(
|
||||||
'httptools>=0.0.9',
|
'httptools>=0.0.9',
|
||||||
'ujson>=1.35',
|
'ujson>=1.35',
|
||||||
'aiofiles>=0.3.0',
|
'aiofiles>=0.3.0',
|
||||||
|
'multidict>=2.0',
|
||||||
],
|
],
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 2 - Pre-Alpha',
|
'Development Status :: 2 - Pre-Alpha',
|
||||||
|
|
|
@ -15,4 +15,4 @@ async def handle(request):
|
||||||
app = web.Application(loop=loop)
|
app = web.Application(loop=loop)
|
||||||
app.router.add_route('GET', '/', handle)
|
app.router.add_route('GET', '/', handle)
|
||||||
|
|
||||||
web.run_app(app, port=sys.argv[1])
|
web.run_app(app, port=sys.argv[1], access_log=None)
|
||||||
|
|
|
@ -1,16 +1,30 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"encoding/json"
|
||||||
"os"
|
"net/http"
|
||||||
"net/http"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type TestJSONResponse struct {
|
||||||
|
Test bool
|
||||||
|
}
|
||||||
|
|
||||||
func handler(w http.ResponseWriter, r *http.Request) {
|
func handler(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
|
response := TestJSONResponse{true}
|
||||||
|
|
||||||
|
js, err := json.Marshal(response)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write(js)
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
http.HandleFunc("/", handler)
|
http.HandleFunc("/", handler)
|
||||||
http.ListenAndServe(":" + os.Args[1], nil)
|
http.ListenAndServe(":"+os.Args[1], nil)
|
||||||
}
|
}
|
||||||
|
|
1
tests/static/decode me.txt
Normal file
1
tests/static/decode me.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
I need to be decoded as a uri
|
1
tests/static/test.file
Normal file
1
tests/static/test.file
Normal file
|
@ -0,0 +1 @@
|
||||||
|
I am just a regular static file
|
20
tests/test_bad_request.py
Normal file
20
tests/test_bad_request.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import asyncio
|
||||||
|
from sanic import Sanic
|
||||||
|
|
||||||
|
|
||||||
|
def test_bad_request_response():
|
||||||
|
app = Sanic('test_bad_request_response')
|
||||||
|
lines = []
|
||||||
|
async def _request(sanic, loop):
|
||||||
|
connect = asyncio.open_connection('127.0.0.1', 42101)
|
||||||
|
reader, writer = await connect
|
||||||
|
writer.write(b'not http')
|
||||||
|
while True:
|
||||||
|
line = await reader.readline()
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
lines.append(line)
|
||||||
|
app.stop()
|
||||||
|
app.run(host='127.0.0.1', port=42101, debug=False, after_start=_request)
|
||||||
|
assert lines[0] == b'HTTP/1.1 400 Bad Request\r\n'
|
||||||
|
assert lines[-1] == b'Error: Bad Request'
|
|
@ -25,6 +25,19 @@ def test_cookies():
|
||||||
assert response.text == 'Cookies are: working!'
|
assert response.text == 'Cookies are: working!'
|
||||||
assert response_cookies['right_back'].value == 'at you'
|
assert response_cookies['right_back'].value == 'at you'
|
||||||
|
|
||||||
|
def test_http2_cookies():
|
||||||
|
app = Sanic('test_http2_cookies')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def handler(request):
|
||||||
|
response = text('Cookies are: {}'.format(request.cookies['test']))
|
||||||
|
return response
|
||||||
|
|
||||||
|
headers = {'cookie': 'test=working!'}
|
||||||
|
request, response = sanic_endpoint_test(app, headers=headers)
|
||||||
|
|
||||||
|
assert response.text == 'Cookies are: working!'
|
||||||
|
|
||||||
def test_cookie_options():
|
def test_cookie_options():
|
||||||
app = Sanic('test_text')
|
app = Sanic('test_text')
|
||||||
|
|
||||||
|
|
32
tests/test_custom_protocol.py
Normal file
32
tests/test_custom_protocol.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.server import HttpProtocol
|
||||||
|
from sanic.response import text
|
||||||
|
from sanic.utils import sanic_endpoint_test
|
||||||
|
|
||||||
|
app = Sanic('test_custom_porotocol')
|
||||||
|
|
||||||
|
|
||||||
|
class CustomHttpProtocol(HttpProtocol):
|
||||||
|
|
||||||
|
def write_response(self, response):
|
||||||
|
if isinstance(response, str):
|
||||||
|
response = text(response)
|
||||||
|
self.transport.write(
|
||||||
|
response.output(self.request.version)
|
||||||
|
)
|
||||||
|
self.transport.close()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/1')
|
||||||
|
async def handler_1(request):
|
||||||
|
return 'OK'
|
||||||
|
|
||||||
|
|
||||||
|
def test_use_custom_protocol():
|
||||||
|
server_kwargs = {
|
||||||
|
'protocol': CustomHttpProtocol
|
||||||
|
}
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/1',
|
||||||
|
server_kwargs=server_kwargs)
|
||||||
|
assert response.status == 200
|
||||||
|
assert response.text == 'OK'
|
|
@ -1,51 +1,86 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
from sanic.exceptions import InvalidUsage, ServerError, NotFound
|
from sanic.exceptions import InvalidUsage, ServerError, NotFound
|
||||||
from sanic.utils import sanic_endpoint_test
|
from sanic.utils import sanic_endpoint_test
|
||||||
|
|
||||||
# ------------------------------------------------------------ #
|
|
||||||
# GET
|
|
||||||
# ------------------------------------------------------------ #
|
|
||||||
|
|
||||||
exception_app = Sanic('test_exceptions')
|
class SanicExceptionTestException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@exception_app.route('/')
|
@pytest.fixture(scope='module')
|
||||||
def handler(request):
|
def exception_app():
|
||||||
return text('OK')
|
app = Sanic('test_exceptions')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def handler(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
@app.route('/error')
|
||||||
|
def handler_error(request):
|
||||||
|
raise ServerError("OK")
|
||||||
|
|
||||||
|
@app.route('/404')
|
||||||
|
def handler_404(request):
|
||||||
|
raise NotFound("OK")
|
||||||
|
|
||||||
|
@app.route('/invalid')
|
||||||
|
def handler_invalid(request):
|
||||||
|
raise InvalidUsage("OK")
|
||||||
|
|
||||||
|
@app.route('/divide_by_zero')
|
||||||
|
def handle_unhandled_exception(request):
|
||||||
|
1 / 0
|
||||||
|
|
||||||
|
@app.route('/error_in_error_handler_handler')
|
||||||
|
def custom_error_handler(request):
|
||||||
|
raise SanicExceptionTestException('Dummy message!')
|
||||||
|
|
||||||
|
@app.exception(SanicExceptionTestException)
|
||||||
|
def error_in_error_handler_handler(request, exception):
|
||||||
|
1 / 0
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
@exception_app.route('/error')
|
def test_no_exception(exception_app):
|
||||||
def handler_error(request):
|
"""Test that a route works without an exception"""
|
||||||
raise ServerError("OK")
|
|
||||||
|
|
||||||
|
|
||||||
@exception_app.route('/404')
|
|
||||||
def handler_404(request):
|
|
||||||
raise NotFound("OK")
|
|
||||||
|
|
||||||
|
|
||||||
@exception_app.route('/invalid')
|
|
||||||
def handler_invalid(request):
|
|
||||||
raise InvalidUsage("OK")
|
|
||||||
|
|
||||||
|
|
||||||
def test_no_exception():
|
|
||||||
request, response = sanic_endpoint_test(exception_app)
|
request, response = sanic_endpoint_test(exception_app)
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert response.text == 'OK'
|
assert response.text == 'OK'
|
||||||
|
|
||||||
|
|
||||||
def test_server_error_exception():
|
def test_server_error_exception(exception_app):
|
||||||
|
"""Test the built-in ServerError exception works"""
|
||||||
request, response = sanic_endpoint_test(exception_app, uri='/error')
|
request, response = sanic_endpoint_test(exception_app, uri='/error')
|
||||||
assert response.status == 500
|
assert response.status == 500
|
||||||
|
|
||||||
|
|
||||||
def test_invalid_usage_exception():
|
def test_invalid_usage_exception(exception_app):
|
||||||
|
"""Test the built-in InvalidUsage exception works"""
|
||||||
request, response = sanic_endpoint_test(exception_app, uri='/invalid')
|
request, response = sanic_endpoint_test(exception_app, uri='/invalid')
|
||||||
assert response.status == 400
|
assert response.status == 400
|
||||||
|
|
||||||
|
|
||||||
def test_not_found_exception():
|
def test_not_found_exception(exception_app):
|
||||||
|
"""Test the built-in NotFound exception works"""
|
||||||
request, response = sanic_endpoint_test(exception_app, uri='/404')
|
request, response = sanic_endpoint_test(exception_app, uri='/404')
|
||||||
assert response.status == 404
|
assert response.status == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_handled_unhandled_exception(exception_app):
|
||||||
|
"""Test that an exception not built into sanic is handled"""
|
||||||
|
request, response = sanic_endpoint_test(
|
||||||
|
exception_app, uri='/divide_by_zero')
|
||||||
|
assert response.status == 500
|
||||||
|
assert response.body == b'An error occurred while generating the response'
|
||||||
|
|
||||||
|
|
||||||
|
def test_exception_in_exception_handler(exception_app):
|
||||||
|
"""Test that an exception thrown in an error handler is handled"""
|
||||||
|
request, response = sanic_endpoint_test(
|
||||||
|
exception_app, uri='/error_in_error_handler_handler')
|
||||||
|
assert response.status == 500
|
||||||
|
assert response.body == b'An error occurred while handling an error'
|
||||||
|
|
33
tests/test_logging.py
Normal file
33
tests/test_logging.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import asyncio
|
||||||
|
from sanic.response import text
|
||||||
|
from sanic import Sanic
|
||||||
|
from io import StringIO
|
||||||
|
from sanic.utils import sanic_endpoint_test
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logging_format = '''module: %(module)s; \
|
||||||
|
function: %(funcName)s(); \
|
||||||
|
message: %(message)s'''
|
||||||
|
|
||||||
|
def test_log():
|
||||||
|
log_stream = StringIO()
|
||||||
|
for handler in logging.root.handlers[:]:
|
||||||
|
logging.root.removeHandler(handler)
|
||||||
|
logging.basicConfig(
|
||||||
|
format=logging_format,
|
||||||
|
level=logging.DEBUG,
|
||||||
|
stream=log_stream
|
||||||
|
)
|
||||||
|
log = logging.getLogger()
|
||||||
|
app = Sanic('test_logging', logger=True)
|
||||||
|
@app.route('/')
|
||||||
|
def handler(request):
|
||||||
|
log.info('hello world')
|
||||||
|
return text('hello')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app)
|
||||||
|
log_text = log_stream.getvalue().strip().split('\n')[-3]
|
||||||
|
assert log_text == "module: test_logging; function: handler(); message: hello world"
|
||||||
|
|
||||||
|
if __name__ =="__main__":
|
||||||
|
test_log()
|
|
@ -1,7 +1,9 @@
|
||||||
from multiprocessing import Array, Event, Process
|
from multiprocessing import Array, Event, Process
|
||||||
from time import sleep
|
from time import sleep, time
|
||||||
from ujson import loads as json_loads
|
from ujson import loads as json_loads
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import json
|
from sanic.response import json
|
||||||
from sanic.utils import local_request, HOST, PORT
|
from sanic.utils import local_request, HOST, PORT
|
||||||
|
@ -13,8 +15,9 @@ from sanic.utils import local_request, HOST, PORT
|
||||||
|
|
||||||
# TODO: Figure out why this freezes on pytest but not when
|
# TODO: Figure out why this freezes on pytest but not when
|
||||||
# executed via interpreter
|
# executed via interpreter
|
||||||
|
@pytest.mark.skip(
|
||||||
def skip_test_multiprocessing():
|
reason="Freezes with pytest not on interpreter")
|
||||||
|
def test_multiprocessing():
|
||||||
app = Sanic('test_json')
|
app = Sanic('test_json')
|
||||||
|
|
||||||
response = Array('c', 50)
|
response = Array('c', 50)
|
||||||
|
@ -51,3 +54,28 @@ def skip_test_multiprocessing():
|
||||||
raise ValueError("Expected JSON response but got '{}'".format(response))
|
raise ValueError("Expected JSON response but got '{}'".format(response))
|
||||||
|
|
||||||
assert results.get('test') == True
|
assert results.get('test') == True
|
||||||
|
|
||||||
|
@pytest.mark.skip(
|
||||||
|
reason="Freezes with pytest not on interpreter")
|
||||||
|
def test_drain_connections():
|
||||||
|
app = Sanic('test_json')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def handler(request):
|
||||||
|
return json({"test": True})
|
||||||
|
|
||||||
|
stop_event = Event()
|
||||||
|
async def after_start(*args, **kwargs):
|
||||||
|
http_response = await local_request('get', '/')
|
||||||
|
stop_event.set()
|
||||||
|
|
||||||
|
start = time()
|
||||||
|
app.serve_multiple({
|
||||||
|
'host': HOST,
|
||||||
|
'port': PORT,
|
||||||
|
'after_start': after_start,
|
||||||
|
'request_handler': app.handle_request,
|
||||||
|
}, workers=2, stop_event=stop_event)
|
||||||
|
end = time()
|
||||||
|
|
||||||
|
assert end - start < 0.05
|
||||||
|
|
54
tests/test_payload_too_large.py
Normal file
54
tests/test_payload_too_large.py
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import text
|
||||||
|
from sanic.exceptions import PayloadTooLarge
|
||||||
|
from sanic.utils import sanic_endpoint_test
|
||||||
|
|
||||||
|
data_received_app = Sanic('data_received')
|
||||||
|
data_received_app.config.REQUEST_MAX_SIZE = 1
|
||||||
|
data_received_default_app = Sanic('data_received_default')
|
||||||
|
data_received_default_app.config.REQUEST_MAX_SIZE = 1
|
||||||
|
on_header_default_app = Sanic('on_header')
|
||||||
|
on_header_default_app.config.REQUEST_MAX_SIZE = 500
|
||||||
|
|
||||||
|
|
||||||
|
@data_received_app.route('/1')
|
||||||
|
async def handler1(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
|
||||||
|
@data_received_app.exception(PayloadTooLarge)
|
||||||
|
def handler_exception(request, exception):
|
||||||
|
return text('Payload Too Large from error_handler.', 413)
|
||||||
|
|
||||||
|
|
||||||
|
def test_payload_too_large_from_error_handler():
|
||||||
|
response = sanic_endpoint_test(
|
||||||
|
data_received_app, uri='/1', gather_request=False)
|
||||||
|
assert response.status == 413
|
||||||
|
assert response.text == 'Payload Too Large from error_handler.'
|
||||||
|
|
||||||
|
|
||||||
|
@data_received_default_app.route('/1')
|
||||||
|
async def handler2(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
|
||||||
|
def test_payload_too_large_at_data_received_default():
|
||||||
|
response = sanic_endpoint_test(
|
||||||
|
data_received_default_app, uri='/1', gather_request=False)
|
||||||
|
assert response.status == 413
|
||||||
|
assert response.text == 'Error: Payload Too Large'
|
||||||
|
|
||||||
|
|
||||||
|
@on_header_default_app.route('/1')
|
||||||
|
async def handler3(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
|
||||||
|
def test_payload_too_large_at_on_header_default():
|
||||||
|
data = 'a' * 1000
|
||||||
|
response = sanic_endpoint_test(
|
||||||
|
on_header_default_app, method='post', uri='/1',
|
||||||
|
gather_request=False, data=data)
|
||||||
|
assert response.status == 413
|
||||||
|
assert response.text == 'Error: Payload Too Large'
|
24
tests/test_request_data.py
Normal file
24
tests/test_request_data.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import json
|
||||||
|
from sanic.utils import sanic_endpoint_test
|
||||||
|
from ujson import loads
|
||||||
|
|
||||||
|
|
||||||
|
def test_storage():
|
||||||
|
app = Sanic('test_text')
|
||||||
|
|
||||||
|
@app.middleware('request')
|
||||||
|
def store(request):
|
||||||
|
request['user'] = 'sanic'
|
||||||
|
request['sidekick'] = 'tails'
|
||||||
|
del request['sidekick']
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def handler(request):
|
||||||
|
return json({ 'user': request.get('user'), 'sidekick': request.get('sidekick') })
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app)
|
||||||
|
|
||||||
|
response_json = loads(response.text)
|
||||||
|
assert response_json['user'] == 'sanic'
|
||||||
|
assert response_json.get('sidekick') is None
|
40
tests/test_request_timeout.py
Normal file
40
tests/test_request_timeout.py
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
from sanic import Sanic
|
||||||
|
import asyncio
|
||||||
|
from sanic.response import text
|
||||||
|
from sanic.exceptions import RequestTimeout
|
||||||
|
from sanic.utils import sanic_endpoint_test
|
||||||
|
from sanic.config import Config
|
||||||
|
|
||||||
|
Config.REQUEST_TIMEOUT = 1
|
||||||
|
request_timeout_app = Sanic('test_request_timeout')
|
||||||
|
request_timeout_default_app = Sanic('test_request_timeout_default')
|
||||||
|
|
||||||
|
|
||||||
|
@request_timeout_app.route('/1')
|
||||||
|
async def handler_1(request):
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
|
||||||
|
@request_timeout_app.exception(RequestTimeout)
|
||||||
|
def handler_exception(request, exception):
|
||||||
|
return text('Request Timeout from error_handler.', 408)
|
||||||
|
|
||||||
|
|
||||||
|
def test_server_error_request_timeout():
|
||||||
|
request, response = sanic_endpoint_test(request_timeout_app, uri='/1')
|
||||||
|
assert response.status == 408
|
||||||
|
assert response.text == 'Request Timeout from error_handler.'
|
||||||
|
|
||||||
|
|
||||||
|
@request_timeout_default_app.route('/1')
|
||||||
|
async def handler_2(request):
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
|
||||||
|
def test_default_server_error_request_timeout():
|
||||||
|
request, response = sanic_endpoint_test(
|
||||||
|
request_timeout_default_app, uri='/1')
|
||||||
|
assert response.status == 408
|
||||||
|
assert response.text == 'Error: Request Timeout'
|
|
@ -2,6 +2,7 @@ from json import loads as json_loads, dumps as json_dumps
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import json, text
|
from sanic.response import json, text
|
||||||
from sanic.utils import sanic_endpoint_test
|
from sanic.utils import sanic_endpoint_test
|
||||||
|
from sanic.exceptions import ServerError
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------ #
|
# ------------------------------------------------------------ #
|
||||||
|
@ -32,6 +33,47 @@ def test_text():
|
||||||
assert response.text == 'Hello'
|
assert response.text == 'Hello'
|
||||||
|
|
||||||
|
|
||||||
|
def test_headers():
|
||||||
|
app = Sanic('test_text')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def handler(request):
|
||||||
|
headers = {"spam": "great"}
|
||||||
|
return text('Hello', headers=headers)
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app)
|
||||||
|
|
||||||
|
assert response.headers.get('spam') == 'great'
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_str_headers():
|
||||||
|
app = Sanic('test_text')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def handler(request):
|
||||||
|
headers = {"answer": 42}
|
||||||
|
return text('Hello', headers=headers)
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app)
|
||||||
|
|
||||||
|
assert response.headers.get('answer') == '42'
|
||||||
|
|
||||||
|
def test_invalid_response():
|
||||||
|
app = Sanic('test_invalid_response')
|
||||||
|
|
||||||
|
@app.exception(ServerError)
|
||||||
|
def handler_exception(request, exception):
|
||||||
|
return text('Internal Server Error.', 500)
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def handler(request):
|
||||||
|
return 'This should fail'
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app)
|
||||||
|
assert response.status == 500
|
||||||
|
assert response.text == "Internal Server Error."
|
||||||
|
|
||||||
|
|
||||||
def test_json():
|
def test_json():
|
||||||
app = Sanic('test_json')
|
app = Sanic('test_json')
|
||||||
|
|
||||||
|
@ -49,6 +91,19 @@ def test_json():
|
||||||
assert results.get('test') == True
|
assert results.get('test') == True
|
||||||
|
|
||||||
|
|
||||||
|
def test_invalid_json():
|
||||||
|
app = Sanic('test_json')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def handler(request):
|
||||||
|
return json(request.json())
|
||||||
|
|
||||||
|
data = "I am not json"
|
||||||
|
request, response = sanic_endpoint_test(app, data=data)
|
||||||
|
|
||||||
|
assert response.status == 400
|
||||||
|
|
||||||
|
|
||||||
def test_query_string():
|
def test_query_string():
|
||||||
app = Sanic('test_query_string')
|
app = Sanic('test_query_string')
|
||||||
|
|
||||||
|
@ -56,12 +111,30 @@ def test_query_string():
|
||||||
async def handler(request):
|
async def handler(request):
|
||||||
return text('OK')
|
return text('OK')
|
||||||
|
|
||||||
request, response = sanic_endpoint_test(app, params=[("test1", 1), ("test2", "false"), ("test2", "true")])
|
request, response = sanic_endpoint_test(app, params=[("test1", "1"), ("test2", "false"), ("test2", "true")])
|
||||||
|
|
||||||
assert request.args.get('test1') == '1'
|
assert request.args.get('test1') == '1'
|
||||||
assert request.args.get('test2') == 'false'
|
assert request.args.get('test2') == 'false'
|
||||||
|
|
||||||
|
|
||||||
|
def test_token():
|
||||||
|
app = Sanic('test_post_token')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
async def handler(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
# uuid4 generated token.
|
||||||
|
token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf'
|
||||||
|
headers = {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'Authorization': 'Token {}'.format(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, headers=headers)
|
||||||
|
|
||||||
|
assert request.token == token
|
||||||
|
|
||||||
# ------------------------------------------------------------ #
|
# ------------------------------------------------------------ #
|
||||||
# POST
|
# POST
|
||||||
# ------------------------------------------------------------ #
|
# ------------------------------------------------------------ #
|
||||||
|
|
18
tests/test_response.py
Normal file
18
tests/test_response.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
from random import choice
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import HTTPResponse
|
||||||
|
from sanic.utils import sanic_endpoint_test
|
||||||
|
|
||||||
|
|
||||||
|
def test_response_body_not_a_string():
|
||||||
|
"""Test when a response body sent from the application is not a string"""
|
||||||
|
app = Sanic('response_body_not_a_string')
|
||||||
|
random_num = choice(range(1000))
|
||||||
|
|
||||||
|
@app.route('/hello')
|
||||||
|
async def hello_route(request):
|
||||||
|
return HTTPResponse(body=random_num)
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/hello')
|
||||||
|
assert response.text == str(random_num)
|
|
@ -2,7 +2,7 @@ import pytest
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
from sanic.router import RouteExists
|
from sanic.router import RouteExists, RouteDoesNotExist
|
||||||
from sanic.utils import sanic_endpoint_test
|
from sanic.utils import sanic_endpoint_test
|
||||||
|
|
||||||
|
|
||||||
|
@ -84,7 +84,7 @@ def test_dynamic_route_int():
|
||||||
|
|
||||||
|
|
||||||
def test_dynamic_route_number():
|
def test_dynamic_route_number():
|
||||||
app = Sanic('test_dynamic_route_int')
|
app = Sanic('test_dynamic_route_number')
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
|
@ -105,7 +105,7 @@ def test_dynamic_route_number():
|
||||||
|
|
||||||
|
|
||||||
def test_dynamic_route_regex():
|
def test_dynamic_route_regex():
|
||||||
app = Sanic('test_dynamic_route_int')
|
app = Sanic('test_dynamic_route_regex')
|
||||||
|
|
||||||
@app.route('/folder/<folder_id:[A-Za-z0-9]{0,4}>')
|
@app.route('/folder/<folder_id:[A-Za-z0-9]{0,4}>')
|
||||||
async def handler(request, folder_id):
|
async def handler(request, folder_id):
|
||||||
|
@ -145,7 +145,7 @@ def test_dynamic_route_unhashable():
|
||||||
|
|
||||||
|
|
||||||
def test_route_duplicate():
|
def test_route_duplicate():
|
||||||
app = Sanic('test_dynamic_route')
|
app = Sanic('test_route_duplicate')
|
||||||
|
|
||||||
with pytest.raises(RouteExists):
|
with pytest.raises(RouteExists):
|
||||||
@app.route('/test')
|
@app.route('/test')
|
||||||
|
@ -178,3 +178,288 @@ def test_method_not_allowed():
|
||||||
|
|
||||||
request, response = sanic_endpoint_test(app, method='post', uri='/test')
|
request, response = sanic_endpoint_test(app, method='post', uri='/test')
|
||||||
assert response.status == 405
|
assert response.status == 405
|
||||||
|
|
||||||
|
|
||||||
|
def test_static_add_route():
|
||||||
|
app = Sanic('test_static_add_route')
|
||||||
|
|
||||||
|
async def handler1(request):
|
||||||
|
return text('OK1')
|
||||||
|
|
||||||
|
async def handler2(request):
|
||||||
|
return text('OK2')
|
||||||
|
|
||||||
|
app.add_route(handler1, '/test')
|
||||||
|
app.add_route(handler2, '/test2')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test')
|
||||||
|
assert response.text == 'OK1'
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test2')
|
||||||
|
assert response.text == 'OK2'
|
||||||
|
|
||||||
|
|
||||||
|
def test_dynamic_add_route():
|
||||||
|
app = Sanic('test_dynamic_add_route')
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
async def handler(request, name):
|
||||||
|
results.append(name)
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
app.add_route(handler, '/folder/<name>')
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test123')
|
||||||
|
|
||||||
|
assert response.text == 'OK'
|
||||||
|
assert results[0] == 'test123'
|
||||||
|
|
||||||
|
|
||||||
|
def test_dynamic_add_route_string():
|
||||||
|
app = Sanic('test_dynamic_add_route_string')
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
async def handler(request, name):
|
||||||
|
results.append(name)
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
app.add_route(handler, '/folder/<name:string>')
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test123')
|
||||||
|
|
||||||
|
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_add_route_int():
|
||||||
|
app = Sanic('test_dynamic_add_route_int')
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
async def handler(request, folder_id):
|
||||||
|
results.append(folder_id)
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
app.add_route(handler, '/folder/<folder_id:int>')
|
||||||
|
|
||||||
|
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_add_route_number():
|
||||||
|
app = Sanic('test_dynamic_add_route_number')
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
async def handler(request, weight):
|
||||||
|
results.append(weight)
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
app.add_route(handler, '/weight/<weight:number>')
|
||||||
|
|
||||||
|
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_add_route_regex():
|
||||||
|
app = Sanic('test_dynamic_route_int')
|
||||||
|
|
||||||
|
async def handler(request, folder_id):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
app.add_route(handler, '/folder/<folder_id:[A-Za-z0-9]{0,4}>')
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def test_dynamic_add_route_unhashable():
|
||||||
|
app = Sanic('test_dynamic_add_route_unhashable')
|
||||||
|
|
||||||
|
async def handler(request, unhashable):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
app.add_route(handler, '/folder/<unhashable:[A-Za-z0-9/]+>/end/')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test/end/')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test/nope/')
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_route_duplicate():
|
||||||
|
app = Sanic('test_add_route_duplicate')
|
||||||
|
|
||||||
|
with pytest.raises(RouteExists):
|
||||||
|
async def handler1(request):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def handler2(request):
|
||||||
|
pass
|
||||||
|
|
||||||
|
app.add_route(handler1, '/test')
|
||||||
|
app.add_route(handler2, '/test')
|
||||||
|
|
||||||
|
with pytest.raises(RouteExists):
|
||||||
|
async def handler1(request, dynamic):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def handler2(request, dynamic):
|
||||||
|
pass
|
||||||
|
|
||||||
|
app.add_route(handler1, '/test/<dynamic>/')
|
||||||
|
app.add_route(handler2, '/test/<dynamic>/')
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_route_method_not_allowed():
|
||||||
|
app = Sanic('test_add_route_method_not_allowed')
|
||||||
|
|
||||||
|
async def handler(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
app.add_route(handler, '/test', methods=['GET'])
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, method='post', uri='/test')
|
||||||
|
assert response.status == 405
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_static_route():
|
||||||
|
app = Sanic('test_remove_static_route')
|
||||||
|
|
||||||
|
async def handler1(request):
|
||||||
|
return text('OK1')
|
||||||
|
|
||||||
|
async def handler2(request):
|
||||||
|
return text('OK2')
|
||||||
|
|
||||||
|
app.add_route(handler1, '/test')
|
||||||
|
app.add_route(handler2, '/test2')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test2')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
app.remove_route('/test')
|
||||||
|
app.remove_route('/test2')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test')
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test2')
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_dynamic_route():
|
||||||
|
app = Sanic('test_remove_dynamic_route')
|
||||||
|
|
||||||
|
async def handler(request, name):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
app.add_route(handler, '/folder/<name>')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test123')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
app.remove_route('/folder/<name>')
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test123')
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_inexistent_route():
|
||||||
|
app = Sanic('test_remove_inexistent_route')
|
||||||
|
|
||||||
|
with pytest.raises(RouteDoesNotExist):
|
||||||
|
app.remove_route('/test')
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_unhashable_route():
|
||||||
|
app = Sanic('test_remove_unhashable_route')
|
||||||
|
|
||||||
|
async def handler(request, unhashable):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
app.add_route(handler, '/folder/<unhashable:[A-Za-z0-9/]+>/end/')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test/end/')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
app.remove_route('/folder/<unhashable:[A-Za-z0-9/]+>/end/')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/')
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test///////end/')
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/folder/test/end/')
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_route_without_clean_cache():
|
||||||
|
app = Sanic('test_remove_static_route')
|
||||||
|
|
||||||
|
async def handler(request):
|
||||||
|
return text('OK')
|
||||||
|
|
||||||
|
app.add_route(handler, '/test')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
app.remove_route('/test', clean_cache=True)
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test')
|
||||||
|
assert response.status == 404
|
||||||
|
|
||||||
|
app.add_route(handler, '/test')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test')
|
||||||
|
assert response.status == 200
|
||||||
|
|
||||||
|
app.remove_route('/test', clean_cache=False)
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test')
|
||||||
|
assert response.status == 200
|
||||||
|
|
|
@ -1,30 +1,62 @@
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.utils import sanic_endpoint_test
|
from sanic.utils import sanic_endpoint_test
|
||||||
|
|
||||||
def test_static_file():
|
|
||||||
current_file = inspect.getfile(inspect.currentframe())
|
|
||||||
with open(current_file, 'rb') as file:
|
|
||||||
current_file_contents = file.read()
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='module')
|
||||||
|
def static_file_directory():
|
||||||
|
"""The static directory to serve"""
|
||||||
|
current_file = inspect.getfile(inspect.currentframe())
|
||||||
|
current_directory = os.path.dirname(os.path.abspath(current_file))
|
||||||
|
static_directory = os.path.join(current_directory, 'static')
|
||||||
|
return static_directory
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='module')
|
||||||
|
def static_file_path(static_file_directory):
|
||||||
|
"""The path to the static file that we want to serve"""
|
||||||
|
return os.path.join(static_file_directory, 'test.file')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='module')
|
||||||
|
def static_file_content(static_file_path):
|
||||||
|
"""The content of the static file to check"""
|
||||||
|
with open(static_file_path, 'rb') as file:
|
||||||
|
return file.read()
|
||||||
|
|
||||||
|
|
||||||
|
def test_static_file(static_file_path, static_file_content):
|
||||||
app = Sanic('test_static')
|
app = Sanic('test_static')
|
||||||
app.static('/testing.file', current_file)
|
app.static('/testing.file', static_file_path)
|
||||||
|
|
||||||
request, response = sanic_endpoint_test(app, uri='/testing.file')
|
request, response = sanic_endpoint_test(app, uri='/testing.file')
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert response.body == current_file_contents
|
assert response.body == static_file_content
|
||||||
|
|
||||||
def test_static_directory():
|
|
||||||
current_file = inspect.getfile(inspect.currentframe())
|
def test_static_directory(
|
||||||
current_directory = os.path.dirname(os.path.abspath(current_file))
|
static_file_directory, static_file_path, static_file_content):
|
||||||
with open(current_file, 'rb') as file:
|
|
||||||
current_file_contents = file.read()
|
|
||||||
|
|
||||||
app = Sanic('test_static')
|
app = Sanic('test_static')
|
||||||
app.static('/dir', current_directory)
|
app.static('/dir', static_file_directory)
|
||||||
|
|
||||||
request, response = sanic_endpoint_test(app, uri='/dir/test_static.py')
|
request, response = sanic_endpoint_test(app, uri='/dir/test.file')
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert response.body == current_file_contents
|
assert response.body == static_file_content
|
||||||
|
|
||||||
|
|
||||||
|
def test_static_url_decode_file(static_file_directory):
|
||||||
|
decode_me_path = os.path.join(static_file_directory, 'decode me.txt')
|
||||||
|
with open(decode_me_path, 'rb') as file:
|
||||||
|
decode_me_contents = file.read()
|
||||||
|
|
||||||
|
app = Sanic('test_static')
|
||||||
|
app.static('/dir', static_file_directory)
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/dir/decode me.txt')
|
||||||
|
assert response.status == 200
|
||||||
|
assert response.body == decode_me_contents
|
||||||
|
|
196
tests/test_views.py
Normal file
196
tests/test_views.py
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import text, HTTPResponse
|
||||||
|
from sanic.views import HTTPMethodView
|
||||||
|
from sanic.blueprints import Blueprint
|
||||||
|
from sanic.request import Request
|
||||||
|
from sanic.utils import sanic_endpoint_test
|
||||||
|
|
||||||
|
|
||||||
|
def test_methods():
|
||||||
|
app = Sanic('test_methods')
|
||||||
|
|
||||||
|
class DummyView(HTTPMethodView):
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return text('I am get method')
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
return text('I am post method')
|
||||||
|
|
||||||
|
def put(self, request):
|
||||||
|
return text('I am put method')
|
||||||
|
|
||||||
|
def patch(self, request):
|
||||||
|
return text('I am patch method')
|
||||||
|
|
||||||
|
def delete(self, request):
|
||||||
|
return text('I am delete method')
|
||||||
|
|
||||||
|
app.add_route(DummyView.as_view(), '/')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, method="get")
|
||||||
|
assert response.text == 'I am get method'
|
||||||
|
request, response = sanic_endpoint_test(app, method="post")
|
||||||
|
assert response.text == 'I am post method'
|
||||||
|
request, response = sanic_endpoint_test(app, method="put")
|
||||||
|
assert response.text == 'I am put method'
|
||||||
|
request, response = sanic_endpoint_test(app, method="patch")
|
||||||
|
assert response.text == 'I am patch method'
|
||||||
|
request, response = sanic_endpoint_test(app, method="delete")
|
||||||
|
assert response.text == 'I am delete method'
|
||||||
|
|
||||||
|
|
||||||
|
def test_unexisting_methods():
|
||||||
|
app = Sanic('test_unexisting_methods')
|
||||||
|
|
||||||
|
class DummyView(HTTPMethodView):
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return text('I am get method')
|
||||||
|
|
||||||
|
app.add_route(DummyView.as_view(), '/')
|
||||||
|
request, response = sanic_endpoint_test(app, method="get")
|
||||||
|
assert response.text == 'I am get method'
|
||||||
|
request, response = sanic_endpoint_test(app, method="post")
|
||||||
|
assert response.text == 'Error: Method POST not allowed for URL /'
|
||||||
|
|
||||||
|
|
||||||
|
def test_argument_methods():
|
||||||
|
app = Sanic('test_argument_methods')
|
||||||
|
|
||||||
|
class DummyView(HTTPMethodView):
|
||||||
|
|
||||||
|
def get(self, request, my_param_here):
|
||||||
|
return text('I am get method with %s' % my_param_here)
|
||||||
|
|
||||||
|
app.add_route(DummyView.as_view(), '/<my_param_here>')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test123')
|
||||||
|
|
||||||
|
assert response.text == 'I am get method with test123'
|
||||||
|
|
||||||
|
|
||||||
|
def test_with_bp():
|
||||||
|
app = Sanic('test_with_bp')
|
||||||
|
bp = Blueprint('test_text')
|
||||||
|
|
||||||
|
class DummyView(HTTPMethodView):
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return text('I am get method')
|
||||||
|
|
||||||
|
bp.add_route(DummyView.as_view(), '/')
|
||||||
|
|
||||||
|
app.blueprint(bp)
|
||||||
|
request, response = sanic_endpoint_test(app)
|
||||||
|
|
||||||
|
assert response.text == 'I am get method'
|
||||||
|
|
||||||
|
|
||||||
|
def test_with_bp_with_url_prefix():
|
||||||
|
app = Sanic('test_with_bp_with_url_prefix')
|
||||||
|
bp = Blueprint('test_text', url_prefix='/test1')
|
||||||
|
|
||||||
|
class DummyView(HTTPMethodView):
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return text('I am get method')
|
||||||
|
|
||||||
|
bp.add_route(DummyView.as_view(), '/')
|
||||||
|
|
||||||
|
app.blueprint(bp)
|
||||||
|
request, response = sanic_endpoint_test(app, uri='/test1/')
|
||||||
|
|
||||||
|
assert response.text == 'I am get method'
|
||||||
|
|
||||||
|
|
||||||
|
def test_with_middleware():
|
||||||
|
app = Sanic('test_with_middleware')
|
||||||
|
|
||||||
|
class DummyView(HTTPMethodView):
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return text('I am get method')
|
||||||
|
|
||||||
|
app.add_route(DummyView.as_view(), '/')
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
@app.middleware
|
||||||
|
async def handler(request):
|
||||||
|
results.append(request)
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app)
|
||||||
|
|
||||||
|
assert response.text == 'I am get method'
|
||||||
|
assert type(results[0]) is Request
|
||||||
|
|
||||||
|
|
||||||
|
def test_with_middleware_response():
|
||||||
|
app = Sanic('test_with_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)
|
||||||
|
|
||||||
|
class DummyView(HTTPMethodView):
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return text('I am get method')
|
||||||
|
|
||||||
|
app.add_route(DummyView.as_view(), '/')
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app)
|
||||||
|
|
||||||
|
assert response.text == 'I am get method'
|
||||||
|
assert type(results[0]) is Request
|
||||||
|
assert type(results[1]) is Request
|
||||||
|
assert issubclass(type(results[2]), HTTPResponse)
|
||||||
|
|
||||||
|
|
||||||
|
def test_with_custom_class_methods():
|
||||||
|
app = Sanic('test_with_custom_class_methods')
|
||||||
|
|
||||||
|
class DummyView(HTTPMethodView):
|
||||||
|
global_var = 0
|
||||||
|
|
||||||
|
def _iternal_method(self):
|
||||||
|
self.global_var += 10
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
self._iternal_method()
|
||||||
|
return text('I am get method and global var is {}'.format(self.global_var))
|
||||||
|
|
||||||
|
app.add_route(DummyView.as_view(), '/')
|
||||||
|
request, response = sanic_endpoint_test(app, method="get")
|
||||||
|
assert response.text == 'I am get method and global var is 10'
|
||||||
|
|
||||||
|
|
||||||
|
def test_with_decorator():
|
||||||
|
app = Sanic('test_with_decorator')
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
def stupid_decorator(view):
|
||||||
|
def decorator(*args, **kwargs):
|
||||||
|
results.append(1)
|
||||||
|
return view(*args, **kwargs)
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
class DummyView(HTTPMethodView):
|
||||||
|
decorators = [stupid_decorator]
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
return text('I am get method')
|
||||||
|
|
||||||
|
app.add_route(DummyView.as_view(), '/')
|
||||||
|
request, response = sanic_endpoint_test(app, method="get")
|
||||||
|
assert response.text == 'I am get method'
|
||||||
|
assert results[0] == 1
|
59
tests/tests_server_events.py
Normal file
59
tests/tests_server_events.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
from io import StringIO
|
||||||
|
from random import choice
|
||||||
|
from string import ascii_letters
|
||||||
|
import signal
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from sanic import Sanic
|
||||||
|
|
||||||
|
AVAILABLE_LISTENERS = [
|
||||||
|
'before_start',
|
||||||
|
'after_start',
|
||||||
|
'before_stop',
|
||||||
|
'after_stop'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def create_listener(listener_name, in_list):
|
||||||
|
async def _listener(app, loop):
|
||||||
|
print('DEBUG MESSAGE FOR PYTEST for {}'.format(listener_name))
|
||||||
|
in_list.insert(0, app.name + listener_name)
|
||||||
|
return _listener
|
||||||
|
|
||||||
|
|
||||||
|
def start_stop_app(random_name_app, **run_kwargs):
|
||||||
|
|
||||||
|
def stop_on_alarm(signum, frame):
|
||||||
|
raise KeyboardInterrupt('SIGINT for sanic to stop gracefully')
|
||||||
|
|
||||||
|
signal.signal(signal.SIGALRM, stop_on_alarm)
|
||||||
|
signal.alarm(1)
|
||||||
|
try:
|
||||||
|
random_name_app.run(**run_kwargs)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('listener_name', AVAILABLE_LISTENERS)
|
||||||
|
def test_single_listener(listener_name):
|
||||||
|
"""Test that listeners on their own work"""
|
||||||
|
random_name_app = Sanic(''.join(
|
||||||
|
[choice(ascii_letters) for _ in range(choice(range(5, 10)))]))
|
||||||
|
output = list()
|
||||||
|
start_stop_app(
|
||||||
|
random_name_app,
|
||||||
|
**{listener_name: create_listener(listener_name, output)})
|
||||||
|
assert random_name_app.name + listener_name == output.pop()
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_listeners():
|
||||||
|
random_name_app = Sanic(''.join(
|
||||||
|
[choice(ascii_letters) for _ in range(choice(range(5, 10)))]))
|
||||||
|
output = list()
|
||||||
|
start_stop_app(
|
||||||
|
random_name_app,
|
||||||
|
**{listener_name: create_listener(listener_name, output)
|
||||||
|
for listener_name in AVAILABLE_LISTENERS})
|
||||||
|
for listener_name in AVAILABLE_LISTENERS:
|
||||||
|
assert random_name_app.name + listener_name == output.pop()
|
33
tox.ini
33
tox.ini
|
@ -1,34 +1,25 @@
|
||||||
[tox]
|
[tox]
|
||||||
|
|
||||||
envlist = py35, report
|
envlist = py35, py36, flake8
|
||||||
|
|
||||||
|
[travis]
|
||||||
|
|
||||||
|
python =
|
||||||
|
3.5: py35, flake8
|
||||||
|
3.6: py36, flake8
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
|
|
||||||
deps =
|
deps =
|
||||||
aiohttp
|
aiohttp
|
||||||
pytest
|
pytest
|
||||||
# pytest-cov
|
|
||||||
coverage
|
|
||||||
|
|
||||||
commands =
|
commands =
|
||||||
coverage run -m pytest tests {posargs}
|
pytest tests {posargs}
|
||||||
mv .coverage .coverage.{envname}
|
|
||||||
|
|
||||||
basepython:
|
[testenv:flake8]
|
||||||
py35: python3.5
|
deps =
|
||||||
|
flake8
|
||||||
whitelist_externals =
|
|
||||||
coverage
|
|
||||||
mv
|
|
||||||
echo
|
|
||||||
|
|
||||||
[testenv:report]
|
|
||||||
|
|
||||||
commands =
|
commands =
|
||||||
coverage combine
|
flake8 sanic
|
||||||
coverage report
|
|
||||||
coverage html
|
|
||||||
echo "Open file://{toxinidir}/coverage/index.html"
|
|
||||||
|
|
||||||
basepython =
|
|
||||||
python3.5
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user