commit
68cb280513
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -14,3 +14,4 @@ settings.py
|
|||
docs/_build/
|
||||
docs/_api/
|
||||
build/*
|
||||
.DS_Store
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
sudo: false
|
||||
dist: precise
|
||||
language: python
|
||||
cache:
|
||||
directories:
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) [year] [fullname]
|
||||
Copyright (c) 2016-present Channel Cat
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
include README.rst
|
||||
include MANIFEST.in
|
||||
include LICENSE
|
||||
include setup.py
|
||||
|
||||
recursive-exclude * __pycache__
|
||||
recursive-exclude * *.py[co]
|
||||
|
|
14
README.rst
14
README.rst
|
@ -31,13 +31,13 @@ Hello World Example
|
|||
Installation
|
||||
------------
|
||||
|
||||
- ``python -m pip install sanic``
|
||||
- ``pip install sanic``
|
||||
|
||||
To install sanic without uvloop or json using bash, you can provide either or both of these environmental variables
|
||||
using any truthy string like `'y', 'yes', 't', 'true', 'on', '1'` and setting the NO_X to true will stop that features
|
||||
installation.
|
||||
|
||||
- ``SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true python -m pip install sanic``
|
||||
- ``SANIC_NO_UVLOOP=true SANIC_NO_UJSON=true pip install sanic``
|
||||
|
||||
|
||||
Documentation
|
||||
|
@ -56,6 +56,16 @@ Documentation
|
|||
.. |PyPI version| image:: https://img.shields.io/pypi/pyversions/sanic.svg
|
||||
:target: https://pypi.python.org/pypi/sanic/
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
`Non-Core examples <https://github.com/channelcat/sanic/wiki/Examples/>`_. Examples of plugins and Sanic that are outside the scope of Sanic core.
|
||||
|
||||
`Extensions <https://github.com/channelcat/sanic/wiki/Extensions/>`_. Sanic extensions created by the community.
|
||||
|
||||
`Projects <https://github.com/channelcat/sanic/wiki/Projects/>`_. Sanic in production use.
|
||||
|
||||
|
||||
TODO
|
||||
----
|
||||
* http2
|
||||
|
|
|
@ -25,7 +25,7 @@ import sanic
|
|||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
extensions = ['sphinx.ext.autodoc']
|
||||
extensions = ['sphinx.ext.autodoc', 'sphinxcontrib.asyncio']
|
||||
|
||||
templates_path = ['_templates']
|
||||
|
||||
|
|
|
@ -93,7 +93,14 @@ def ignore_404s(request, exception):
|
|||
Static files can be served globally, under the blueprint prefix.
|
||||
|
||||
```python
|
||||
bp.static('/folder/to/serve', '/web/path')
|
||||
|
||||
# suppose bp.name == 'bp'
|
||||
|
||||
bp.static('/web/path', '/folder/to/serve')
|
||||
# also you can pass name parameter to it for url_for
|
||||
bp.static('/web/path', '/folder/to/server', name='uploads')
|
||||
app.url_for('static', name='bp.uploads', filename='file.txt') == '/bp/web/path/file.txt'
|
||||
|
||||
```
|
||||
|
||||
## Start and stop
|
||||
|
@ -172,7 +179,7 @@ takes the format `<blueprint_name>.<handler_name>`. For example:
|
|||
```python
|
||||
@blueprint_v1.route('/')
|
||||
async def root(request):
|
||||
url = app.url_for('v1.post_handler', post_id=5) # --> '/v1/post/5'
|
||||
url = request.app.url_for('v1.post_handler', post_id=5) # --> '/v1/post/5'
|
||||
return redirect(url)
|
||||
|
||||
|
||||
|
|
|
@ -29,9 +29,15 @@ In general the convention is to only have UPPERCASE configuration parameters. Th
|
|||
|
||||
There are several ways how to load configuration.
|
||||
|
||||
### From environment variables.
|
||||
### From Environment Variables
|
||||
|
||||
Any variables defined with the `SANIC_` prefix will be applied to the sanic config. For example, setting `SANIC_REQUEST_TIMEOUT` will be loaded by the application automatically. You can pass the `load_env` boolean to the Sanic constructor to override that:
|
||||
Any variables defined with the `SANIC_` prefix will be applied to the sanic config. For example, setting `SANIC_REQUEST_TIMEOUT` will be loaded by the application automatically and fed into the `REQUEST_TIMEOUT` config variable. You can pass a different prefix to Sanic:
|
||||
|
||||
```python
|
||||
app = Sanic(load_env='MYAPP_')
|
||||
```
|
||||
|
||||
Then the above variable would be `MYAPP_REQUEST_TIMEOUT`. If you want to disable loading from environment variables you can set it to `False` instead:
|
||||
|
||||
```python
|
||||
app = Sanic(load_env=False)
|
||||
|
@ -79,8 +85,35 @@ DB_USER = 'appuser'
|
|||
|
||||
Out of the box there are just a few predefined values which can be overwritten when creating the application.
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ----------------- | --------- | --------------------------------- |
|
||||
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
|
||||
| REQUEST_TIMEOUT | 60 | How long a request can take (sec) |
|
||||
| KEEP_ALIVE | True | Disables keep-alive when False |
|
||||
| Variable | Default | Description |
|
||||
| ------------------ | --------- | --------------------------------------------- |
|
||||
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
|
||||
| REQUEST_TIMEOUT | 60 | How long a request can take to arrive (sec) |
|
||||
| RESPONSE_TIMEOUT | 60 | How long a response can take to process (sec) |
|
||||
| KEEP_ALIVE | True | Disables keep-alive when False |
|
||||
| KEEP_ALIVE_TIMEOUT | 5 | How long to hold a TCP connection open (sec) |
|
||||
|
||||
### The different Timeout variables:
|
||||
|
||||
A request timeout measures the duration of time between the instant when a new open TCP connection is passed to the Sanic backend server, and the instant when the whole HTTP request is received. If the time taken exceeds the `REQUEST_TIMEOUT` value (in seconds), this is considered a Client Error so Sanic generates a HTTP 408 response and sends that to the client. Adjust this value higher if your clients routinely pass very large request payloads or upload requests very slowly.
|
||||
|
||||
A response timeout measures the duration of time between the instant the Sanic server passes the HTTP request to the Sanic App, and the instant a HTTP response is sent to the client. If the time taken exceeds the `RESPONSE_TIMEOUT` value (in seconds), this is considered a Server Error so Sanic generates a HTTP 503 response and sets that to the client. Adjust this value higher if your application is likely to have long-running process that delay the generation of a response.
|
||||
|
||||
### What is Keep Alive? And what does the Keep Alive Timeout value do?
|
||||
|
||||
Keep-Alive is a HTTP feature indroduced in HTTP 1.1. When sending a HTTP request, the client (usually a web browser application) can set a Keep-Alive header to indicate for the http server (Sanic) to not close the TCP connection after it has send the response. This allows the client to reuse the existing TCP connection to send subsequent HTTP requests, and ensures more efficient network traffic for both the client and the server.
|
||||
|
||||
The `KEEP_ALIVE` config variable is set to `True` in Sanic by default. If you don't need this feature in your application, set it to `False` to cause all client connections to close immediately after a response is sent, regardless of the Keep-Alive header on the request.
|
||||
|
||||
The amount of time the server holds the TCP connection open is decided by the server itself. In Sanic, that value is configured using the `KEEP_ALIVE_TIMEOUT` value. By default, it is set to 5 seconds, this is the same default setting as the Apache HTTP server and is a good balance between allowing enough time for the client to send a new request, and not holding open too many connections at once. Do not exceed 75 seconds unless you know your clients are using a browser which supports TCP connections held open for that long.
|
||||
|
||||
For reference:
|
||||
```
|
||||
Apache httpd server default keepalive timeout = 5 seconds
|
||||
Nginx server default keepalive timeout = 75 seconds
|
||||
Nginx performance tuning guidelines uses keepalive = 15 seconds
|
||||
IE (5-9) client hard keepalive limit = 60 seconds
|
||||
Firefox client hard keepalive limit = 115 seconds
|
||||
Opera 11 client hard keepalive limit = 120 seconds
|
||||
Chrome 13+ client keepalive limit > 300+ seconds
|
||||
```
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
# Extensions
|
||||
|
||||
A list of Sanic extensions created by the community.
|
||||
|
||||
- [Sanic-Plugins-Framework](https://github.com/ashleysommer/sanicpluginsframework): Library for easily creating and using Sanic plugins.
|
||||
- [Sessions](https://github.com/subyraman/sanic_session): Support for sessions.
|
||||
Allows using redis, memcache or an in memory store.
|
||||
- [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors.
|
||||
- [Compress](https://github.com/subyraman/sanic_compress): Allows you to easily gzip Sanic responses. A port of Flask-Compress.
|
||||
- [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template.
|
||||
- [JWT](https://github.com/ahopkins/sanic-jwt): Authentication extension for JSON Web Tokens (JWT).
|
||||
- [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI.
|
||||
- [Pagination](https://github.com/lixxu/python-paginate): Simple pagination support.
|
||||
- [Motor](https://github.com/lixxu/sanic-motor): Simple motor wrapper.
|
||||
|
|
|
@ -9,15 +9,16 @@ syntax, so earlier versions of python won't work.
|
|||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.response import json
|
||||
|
||||
app = Sanic(__name__)
|
||||
app = Sanic()
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
return text('Hello world!')
|
||||
return json({"hello": "world"})
|
||||
|
||||
app.run(host="0.0.0.0", port=8000, debug=True)
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
3. Run the server: `python3 main.py`
|
||||
|
|
|
@ -9,12 +9,6 @@ A simple example using default settings would be like this:
|
|||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.config import LOGGING
|
||||
|
||||
# The default logging handlers are ['accessStream', 'errorStream']
|
||||
# but we change it to use other handlers here for demo purpose
|
||||
LOGGING['loggers']['network']['handlers'] = [
|
||||
'accessSysLog', 'errorSysLog']
|
||||
|
||||
app = Sanic('test')
|
||||
|
||||
|
@ -23,14 +17,21 @@ async def test(request):
|
|||
return response.text('Hello World!')
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(log_config=LOGGING)
|
||||
app.run(debug=True, access_log=True)
|
||||
```
|
||||
|
||||
And to close logging, simply assign log_config=None:
|
||||
To use your own logging config, simply use `logging.config.dictConfig`, or
|
||||
pass `log_config` when you initialize `Sanic` app:
|
||||
|
||||
```python
|
||||
app = Sanic('test', log_config=LOGGING_CONFIG)
|
||||
```
|
||||
|
||||
And to close logging, simply assign access_log=False:
|
||||
|
||||
```python
|
||||
if __name__ == "__main__":
|
||||
app.run(log_config=None)
|
||||
app.run(access_log=False)
|
||||
```
|
||||
|
||||
This would skip calling logging functions when handling requests.
|
||||
|
@ -38,64 +39,29 @@ And you could even do further in production to gain extra speed:
|
|||
|
||||
```python
|
||||
if __name__ == "__main__":
|
||||
# disable internal messages
|
||||
app.run(debug=False, log_config=None)
|
||||
# disable debug messages
|
||||
app.run(debug=False, access_log=False)
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
By default, log_config parameter is set to use sanic.config.LOGGING dictionary for configuration. The default configuration provides several predefined `handlers`:
|
||||
By default, log_config parameter is set to use sanic.log.LOGGING_CONFIG_DEFAULTS dictionary for configuration.
|
||||
|
||||
- internal (using [logging.StreamHandler](https://docs.python.org/3/library/logging.handlers.html#logging.StreamHandler))<br>
|
||||
For internal information console outputs.
|
||||
There are three `loggers` used in sanic, and **must be defined if you want to create your own logging configuration**:
|
||||
|
||||
|
||||
- accessStream (using [logging.StreamHandler](https://docs.python.org/3/library/logging.handlers.html#logging.StreamHandler))<br>
|
||||
For requests information logging in console
|
||||
|
||||
|
||||
- errorStream (using [logging.StreamHandler](https://docs.python.org/3/library/logging.handlers.html#logging.StreamHandler))<br>
|
||||
For error message and traceback logging in console.
|
||||
|
||||
|
||||
- accessSysLog (using [logging.handlers.SysLogHandler](https://docs.python.org/3/library/logging.handlers.html#logging.handlers.SysLogHandler))<br>
|
||||
For requests information logging to syslog.
|
||||
Currently supports Windows (via localhost:514), Darwin (/var/run/syslog),
|
||||
Linux (/dev/log) and FreeBSD (/dev/log).<br>
|
||||
You would not be able to access this property if the directory doesn't exist.
|
||||
(Notice that in Docker you have to enable everything by yourself)
|
||||
|
||||
|
||||
- errorSysLog (using [logging.handlers.SysLogHandler](https://docs.python.org/3/library/logging.handlers.html#logging.handlers.SysLogHandler))<br>
|
||||
For error message and traceback logging to syslog.
|
||||
Currently supports Windows (via localhost:514), Darwin (/var/run/syslog),
|
||||
Linux (/dev/log) and FreeBSD (/dev/log).<br>
|
||||
You would not be able to access this property if the directory doesn't exist.
|
||||
(Notice that in Docker you have to enable everything by yourself)
|
||||
|
||||
|
||||
And `filters`:
|
||||
|
||||
- accessFilter (using sanic.log.DefaultFilter)<br>
|
||||
The filter that allows only levels in `DEBUG`, `INFO`, and `NONE(0)`
|
||||
|
||||
|
||||
- errorFilter (using sanic.log.DefaultFilter)<br>
|
||||
The filter that allows only levels in `WARNING`, `ERROR`, and `CRITICAL`
|
||||
|
||||
There are two `loggers` used in sanic, and **must be defined if you want to create your own logging configuration**:
|
||||
|
||||
- sanic:<br>
|
||||
- root:<br>
|
||||
Used to log internal messages.
|
||||
|
||||
- sanic.error:<br>
|
||||
Used to log error logs.
|
||||
|
||||
- network:<br>
|
||||
Used to log requests from network, and any information from those requests.
|
||||
- sanic.access:<br>
|
||||
Used to log access logs.
|
||||
|
||||
#### Log format:
|
||||
|
||||
In addition to default parameters provided by python (asctime, levelname, message),
|
||||
Sanic provides additional parameters for network logger with accessFilter:
|
||||
Sanic provides additional parameters for access logger with:
|
||||
|
||||
- host (str)<br>
|
||||
request.ip
|
||||
|
|
|
@ -4,7 +4,7 @@ Middleware are functions which are executed before or after requests to the
|
|||
server. They can be used to modify the *request to* or *response from*
|
||||
user-defined handler functions.
|
||||
|
||||
Additionally, Sanic providers listeners which allow you to run code at various points of your application's lifecycle.
|
||||
Additionally, Sanic provides listeners which allow you to run code at various points of your application's lifecycle.
|
||||
|
||||
## Middleware
|
||||
|
||||
|
|
|
@ -71,8 +71,14 @@ The following variables are accessible as properties on `Request` objects:
|
|||
return text("You are trying to create a user with the following POST: %s" % request.body)
|
||||
```
|
||||
|
||||
- `headers` (dict) - A case-insensitive dictionary that contains the request headers.
|
||||
|
||||
- `ip` (str) - IP address of the requester.
|
||||
|
||||
- `port` (str) - Port address of the requester.
|
||||
|
||||
- `socket` (tuple) - (IP, port) of the requester.
|
||||
|
||||
- `app` - a reference to the Sanic application object that is handling this request. This is useful when inside blueprints or other handlers in modules that do not have access to the global `app` object.
|
||||
|
||||
```python
|
||||
|
@ -95,6 +101,7 @@ The following variables are accessible as properties on `Request` objects:
|
|||
- `path`: The path of the request: `/posts/1/`
|
||||
- `query_string`: The query string of the request: `foo=bar` or a blank string `''`
|
||||
- `uri_template`: Template for matching route handler: `/posts/<id>/`
|
||||
- `token`: The value of Authorization header: `Basic YWRtaW46YWRtaW4=`
|
||||
|
||||
|
||||
## Accessing values using `get` and `getlist`
|
||||
|
|
|
@ -214,3 +214,121 @@ and `recv` methods to send and receive data respectively.
|
|||
|
||||
WebSocket support requires the [websockets](https://github.com/aaugustin/websockets)
|
||||
package by Aymeric Augustin.
|
||||
|
||||
|
||||
## About `strict_slashes`
|
||||
|
||||
You can make `routes` strict to trailing slash or not, it's configurable.
|
||||
|
||||
```python
|
||||
|
||||
# provide default strict_slashes value for all routes
|
||||
app = Sanic('test_route_strict_slash', strict_slashes=True)
|
||||
|
||||
# you can also overwrite strict_slashes value for specific route
|
||||
@app.get('/get', strict_slashes=False)
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
# It also works for blueprints
|
||||
bp = Blueprint('test_bp_strict_slash', strict_slashes=True)
|
||||
|
||||
@bp.get('/bp/get', strict_slashes=False)
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
app.blueprint(bp)
|
||||
```
|
||||
|
||||
## User defined route name
|
||||
|
||||
You can pass `name` to change the route name to avoid using the default name (`handler.__name__`).
|
||||
|
||||
```python
|
||||
|
||||
app = Sanic('test_named_route')
|
||||
|
||||
@app.get('/get', name='get_handler')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
# then you need use `app.url_for('get_handler')`
|
||||
# instead of # `app.url_for('handler')`
|
||||
|
||||
# It also works for blueprints
|
||||
bp = Blueprint('test_named_bp')
|
||||
|
||||
@bp.get('/bp/get', name='get_handler')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
# then you need use `app.url_for('test_named_bp.get_handler')`
|
||||
# instead of `app.url_for('test_named_bp.handler')`
|
||||
|
||||
# different names can be used for same url with different methods
|
||||
|
||||
@app.get('/test', name='route_test')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
@app.post('/test', name='route_post')
|
||||
def handler2(request):
|
||||
return text('OK POST')
|
||||
|
||||
@app.put('/test', name='route_put')
|
||||
def handler3(request):
|
||||
return text('OK PUT')
|
||||
|
||||
# below url are the same, you can use any of them
|
||||
# '/test'
|
||||
app.url_for('route_test')
|
||||
# app.url_for('route_post')
|
||||
# app.url_for('route_put')
|
||||
|
||||
# for same handler name with different methods
|
||||
# you need specify the name (it's url_for issue)
|
||||
@app.get('/get')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
@app.post('/post', name='post_handler')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
# then
|
||||
# app.url_for('handler') == '/get'
|
||||
# app.url_for('post_handler') == '/post'
|
||||
```
|
||||
|
||||
## Build URL for static files
|
||||
|
||||
You can use `url_for` for static file url building now.
|
||||
If it's for file directly, `filename` can be ignored.
|
||||
|
||||
```python
|
||||
|
||||
app = Sanic('test_static')
|
||||
app.static('/static', './static')
|
||||
app.static('/uploads', './uploads', name='uploads')
|
||||
app.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
|
||||
|
||||
bp = Blueprint('bp', url_prefix='bp')
|
||||
bp.static('/static', './static')
|
||||
bp.static('/uploads', './uploads', name='uploads')
|
||||
bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
|
||||
app.blueprint(bp)
|
||||
|
||||
# then build the url
|
||||
app.url_for('static', filename='file.txt') == '/static/file.txt'
|
||||
app.url_for('static', name='static', filename='file.txt') == '/static/file.txt'
|
||||
app.url_for('static', name='uploads', filename='file.txt') == '/uploads/file.txt'
|
||||
app.url_for('static', name='best_png') == '/the_best.png'
|
||||
|
||||
# blueprint url building
|
||||
app.url_for('static', name='bp.static', filename='file.txt') == '/bp/static/file.txt'
|
||||
app.url_for('static', name='bp.uploads', filename='file.txt') == '/bp/uploads/file.txt'
|
||||
app.url_for('static', name='bp.best_png') == '/bp/static/the_best.png'
|
||||
|
||||
```
|
||||
|
|
|
@ -6,16 +6,40 @@ filename. The file specified will then be accessible via the given endpoint.
|
|||
|
||||
```python
|
||||
from sanic import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
# Serves files from the static folder to the URL /static
|
||||
app.static('/static', './static')
|
||||
# use url_for to build the url, name defaults to 'static' and can be ignored
|
||||
app.url_for('static', filename='file.txt') == '/static/file.txt'
|
||||
app.url_for('static', name='static', filename='file.txt') == '/static/file.txt'
|
||||
|
||||
# Serves the file /home/ubuntu/test.png when the URL /the_best.png
|
||||
# is requested
|
||||
app.static('/the_best.png', '/home/ubuntu/test.png')
|
||||
app.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
|
||||
|
||||
# you can use url_for to build the static file url
|
||||
# you can ignore name and filename parameters if you don't define it
|
||||
app.url_for('static', name='best_png') == '/the_best.png'
|
||||
app.url_for('static', name='best_png', filename='any') == '/the_best.png'
|
||||
|
||||
# you need define the name for other static files
|
||||
app.static('/another.png', '/home/ubuntu/another.png', name='another')
|
||||
app.url_for('static', name='another') == '/another.png'
|
||||
app.url_for('static', name='another', filename='any') == '/another.png'
|
||||
|
||||
# also, you can use static for blueprint
|
||||
bp = Blueprint('bp', url_prefix='/bp')
|
||||
bp.static('/static', './static')
|
||||
|
||||
# servers the file directly
|
||||
bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
|
||||
app.blueprint(bp)
|
||||
|
||||
app.url_for('static', name='bp.static', filename='file.txt') == '/bp/static/file.txt'
|
||||
app.url_for('static', name='bp.best_png') == '/bp/test_best.png'
|
||||
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
Note: currently you cannot build a URL for a static file using `url_for`.
|
||||
|
|
|
@ -20,7 +20,7 @@ def test_index_put_not_allowed():
|
|||
assert response.status == 405
|
||||
```
|
||||
|
||||
Internally, each time you call one of the `test_client` methods, the Sanic app is run at `127.0.01:42101` and
|
||||
Internally, each time you call one of the `test_client` methods, the Sanic app is run at `127.0.0.1:42101` and
|
||||
your test request is executed against your application, using `aiohttp`.
|
||||
|
||||
The `test_client` methods accept the following arguments and keyword arguments:
|
||||
|
@ -59,7 +59,7 @@ the available arguments to aiohttp can be found
|
|||
[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session).
|
||||
|
||||
|
||||
# pytest-sanic
|
||||
## pytest-sanic
|
||||
|
||||
[pytest-sanic](https://github.com/yunstanford/pytest-sanic) is a pytest plugin, it helps you to test your code asynchronously.
|
||||
Just write tests like,
|
||||
|
|
|
@ -10,11 +10,11 @@ You can pass a version number to the routes directly.
|
|||
from sanic import response
|
||||
|
||||
|
||||
@app.route('/text', verion=1)
|
||||
@app.route('/text', version=1)
|
||||
def handle_request(request):
|
||||
return response.text('Hello world! Version 1')
|
||||
|
||||
@app.route('/text', verion=2)
|
||||
@app.route('/text', version=2)
|
||||
def handle_request(request):
|
||||
return response.text('Hello world! Version 2')
|
||||
|
||||
|
|
17
examples/add_task_sanic.py
Normal file
17
examples/add_task_sanic.py
Normal file
|
@ -0,0 +1,17 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import asyncio
|
||||
|
||||
from sanic import Sanic
|
||||
|
||||
app = Sanic()
|
||||
|
||||
|
||||
async def notify_server_started_after_five_seconds():
|
||||
await asyncio.sleep(5)
|
||||
print('Server successfully started!')
|
||||
|
||||
app.add_task(notify_server_started_after_five_seconds())
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
42
examples/authorized_sanic.py
Normal file
42
examples/authorized_sanic.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from sanic import Sanic
|
||||
from functools import wraps
|
||||
from sanic.response import json
|
||||
|
||||
app = Sanic()
|
||||
|
||||
|
||||
def check_request_for_authorization_status(request):
|
||||
# Note: Define your check, for instance cookie, session.
|
||||
flag = True
|
||||
return flag
|
||||
|
||||
|
||||
def authorized():
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
async def decorated_function(request, *args, **kwargs):
|
||||
# run some method that checks the request
|
||||
# for the client's authorization status
|
||||
is_authorized = check_request_for_authorization_status(request)
|
||||
|
||||
if is_authorized:
|
||||
# the user is authorized.
|
||||
# run the handler method and return the response
|
||||
response = await f(request, *args, **kwargs)
|
||||
return response
|
||||
else:
|
||||
# the user is not authorized.
|
||||
return json({'status': 'not_authorized'}, 403)
|
||||
return decorated_function
|
||||
return decorator
|
||||
|
||||
|
||||
@app.route("/")
|
||||
@authorized()
|
||||
async def test(request):
|
||||
return json({'status': 'authorized'})
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=8000)
|
86
examples/log_request_id.py
Normal file
86
examples/log_request_id.py
Normal file
|
@ -0,0 +1,86 @@
|
|||
'''
|
||||
Based on example from https://github.com/Skyscanner/aiotask-context
|
||||
and `examples/{override_logging,run_async}.py`.
|
||||
|
||||
Needs https://github.com/Skyscanner/aiotask-context/tree/52efbc21e2e1def2d52abb9a8e951f3ce5e6f690 or newer
|
||||
|
||||
$ pip install git+https://github.com/Skyscanner/aiotask-context.git
|
||||
'''
|
||||
|
||||
import asyncio
|
||||
import uuid
|
||||
import logging
|
||||
from signal import signal, SIGINT
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic import response
|
||||
|
||||
import uvloop
|
||||
import aiotask_context as context
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RequestIdFilter(logging.Filter):
|
||||
def filter(self, record):
|
||||
record.request_id = context.get('X-Request-ID')
|
||||
return True
|
||||
|
||||
|
||||
LOG_SETTINGS = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'handlers': {
|
||||
'console': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'level': 'DEBUG',
|
||||
'formatter': 'default',
|
||||
'filters': ['requestid'],
|
||||
},
|
||||
},
|
||||
'filters': {
|
||||
'requestid': {
|
||||
'()': RequestIdFilter,
|
||||
},
|
||||
},
|
||||
'formatters': {
|
||||
'default': {
|
||||
'format': '%(asctime)s %(levelname)s %(name)s:%(lineno)d %(request_id)s | %(message)s',
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'': {
|
||||
'level': 'DEBUG',
|
||||
'handlers': ['console'],
|
||||
'propagate': True
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
app = Sanic(__name__, log_config=LOG_SETTINGS)
|
||||
|
||||
|
||||
@app.middleware('request')
|
||||
async def set_request_id(request):
|
||||
request_id = request.headers.get('X-Request-ID') or str(uuid.uuid4())
|
||||
context.set("X-Request-ID", request_id)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(request):
|
||||
log.debug('X-Request-ID: %s', context.get('X-Request-ID'))
|
||||
log.info('Hello from test!')
|
||||
return response.json({"test": True})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.set_event_loop(uvloop.new_event_loop())
|
||||
server = app.create_server(host="0.0.0.0", port=8000)
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.set_task_factory(context.task_factory)
|
||||
task = asyncio.ensure_future(server)
|
||||
try:
|
||||
loop.run_forever()
|
||||
except:
|
||||
loop.stop()
|
49
examples/pytest_xdist.py
Normal file
49
examples/pytest_xdist.py
Normal file
|
@ -0,0 +1,49 @@
|
|||
"""pytest-xdist example for sanic server
|
||||
|
||||
Install testing tools:
|
||||
|
||||
$ pip install pytest pytest-xdist
|
||||
|
||||
Run with xdist params:
|
||||
|
||||
$ pytest examples/pytest_xdist.py -n 8 # 8 workers
|
||||
"""
|
||||
import re
|
||||
from sanic import Sanic
|
||||
from sanic.response import text
|
||||
from sanic.testing import PORT as PORT_BASE, SanicTestClient
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def test_port(worker_id):
|
||||
m = re.search(r'[0-9]+', worker_id)
|
||||
if m:
|
||||
num_id = m.group(0)
|
||||
else:
|
||||
num_id = 0
|
||||
port = PORT_BASE + int(num_id)
|
||||
return port
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def app():
|
||||
app = Sanic()
|
||||
|
||||
@app.route('/')
|
||||
async def index(request):
|
||||
return text('OK')
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def client(app, test_port):
|
||||
return SanicTestClient(app, test_port)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('run_id', range(100))
|
||||
def test_index(client, run_id):
|
||||
request, response = client._sanic_endpoint_test('get', '/')
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
13
examples/teapot.py
Normal file
13
examples/teapot.py
Normal file
|
@ -0,0 +1,13 @@
|
|||
from sanic import Sanic
|
||||
from sanic import response as res
|
||||
|
||||
app = Sanic(__name__)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
async def test(req):
|
||||
return res.text("I\'m a teapot", status=418)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=8000)
|
|
@ -18,7 +18,7 @@ def test_sync(request):
|
|||
return response.json({"test": True})
|
||||
|
||||
|
||||
@app.route("/dynamic/<name>/<id:int>")
|
||||
@app.route("/dynamic/<name>/<i:int>")
|
||||
def test_params(request, name, i):
|
||||
return response.text("yeehaww {} {}".format(name, i))
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
sphinx
|
||||
sphinx_rtd_theme
|
||||
recommonmark
|
||||
sphinxcontrib-asyncio
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
from sanic.app import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
|
||||
__version__ = '0.5.4'
|
||||
__version__ = '0.7.0'
|
||||
|
||||
__all__ = ['Sanic', 'Blueprint']
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from argparse import ArgumentParser
|
||||
from importlib import import_module
|
||||
|
||||
from sanic.log import log
|
||||
from sanic.log import logger
|
||||
from sanic.app import Sanic
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -36,9 +36,9 @@ if __name__ == "__main__":
|
|||
app.run(host=args.host, port=args.port,
|
||||
workers=args.workers, debug=args.debug, ssl=ssl)
|
||||
except ImportError as e:
|
||||
log.error("No module named {} found.\n"
|
||||
" Example File: project/sanic_server.py -> app\n"
|
||||
" Example Module: project.sanic_server.app"
|
||||
.format(e.name))
|
||||
logger.error("No module named {} found.\n"
|
||||
" Example File: project/sanic_server.py -> app\n"
|
||||
" Example Module: project.sanic_server.app"
|
||||
.format(e.name))
|
||||
except ValueError as e:
|
||||
log.error("{}".format(e))
|
||||
logger.error("{}".format(e))
|
||||
|
|
216
sanic/app.py
216
sanic/app.py
|
@ -10,11 +10,11 @@ from traceback import format_exc
|
|||
from urllib.parse import urlencode, urlunparse
|
||||
from ssl import create_default_context, Purpose
|
||||
|
||||
from sanic.config import Config, LOGGING
|
||||
from sanic.config import Config
|
||||
from sanic.constants import HTTP_METHODS
|
||||
from sanic.exceptions import ServerError, URLBuildError, SanicException
|
||||
from sanic.handlers import ErrorHandler
|
||||
from sanic.log import log
|
||||
from sanic.log import logger, error_logger, LOGGING_CONFIG_DEFAULTS
|
||||
from sanic.response import HTTPResponse, StreamingHTTPResponse
|
||||
from sanic.router import Router
|
||||
from sanic.server import serve, serve_multiple, HttpProtocol, Signal
|
||||
|
@ -28,43 +28,36 @@ class Sanic:
|
|||
|
||||
def __init__(self, name=None, router=None, error_handler=None,
|
||||
load_env=True, request_class=None,
|
||||
log_config=LOGGING):
|
||||
if log_config:
|
||||
logging.config.dictConfig(log_config)
|
||||
# Only set up a default log handler if the
|
||||
# end-user application didn't set anything up.
|
||||
if not (logging.root.handlers and
|
||||
log.level == logging.NOTSET and
|
||||
log_config):
|
||||
formatter = logging.Formatter(
|
||||
"%(asctime)s: %(levelname)s: %(message)s")
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(formatter)
|
||||
log.addHandler(handler)
|
||||
log.setLevel(logging.INFO)
|
||||
strict_slashes=False, log_config=None,
|
||||
configure_logging=True):
|
||||
|
||||
# Get name from previous stack frame
|
||||
if name is None:
|
||||
frame_records = stack()[1]
|
||||
name = getmodulename(frame_records[1])
|
||||
|
||||
# logging
|
||||
if configure_logging:
|
||||
logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS)
|
||||
|
||||
self.name = name
|
||||
self.router = router or Router()
|
||||
self.request_class = request_class
|
||||
self.error_handler = error_handler or ErrorHandler()
|
||||
self.config = Config(load_env=load_env)
|
||||
self.log_config = log_config
|
||||
self.request_middleware = deque()
|
||||
self.response_middleware = deque()
|
||||
self.blueprints = {}
|
||||
self._blueprint_order = []
|
||||
self.configure_logging = configure_logging
|
||||
self.debug = None
|
||||
self.sock = None
|
||||
self.strict_slashes = strict_slashes
|
||||
self.listeners = defaultdict(list)
|
||||
self.is_running = False
|
||||
self.is_request_stream = False
|
||||
self.websocket_enabled = False
|
||||
self.websocket_tasks = []
|
||||
self.websocket_tasks = set()
|
||||
|
||||
# Register alternative method names
|
||||
self.go_fast = self.run
|
||||
|
@ -113,7 +106,7 @@ class Sanic:
|
|||
|
||||
# Decorator
|
||||
def route(self, uri, methods=frozenset({'GET'}), host=None,
|
||||
strict_slashes=False, stream=False, version=None):
|
||||
strict_slashes=None, stream=False, version=None, name=None):
|
||||
"""Decorate a function to be registered as a route
|
||||
|
||||
:param uri: path of the URL
|
||||
|
@ -121,6 +114,8 @@ class Sanic:
|
|||
:param host:
|
||||
:param strict_slashes:
|
||||
:param stream:
|
||||
:param version:
|
||||
:param name: user defined route name for url_for
|
||||
:return: decorated function
|
||||
"""
|
||||
|
||||
|
@ -132,53 +127,64 @@ class Sanic:
|
|||
if stream:
|
||||
self.is_request_stream = True
|
||||
|
||||
if strict_slashes is None:
|
||||
strict_slashes = self.strict_slashes
|
||||
|
||||
def response(handler):
|
||||
if stream:
|
||||
handler.is_stream = stream
|
||||
self.router.add(uri=uri, methods=methods, handler=handler,
|
||||
host=host, strict_slashes=strict_slashes,
|
||||
version=version)
|
||||
version=version, name=name)
|
||||
return handler
|
||||
|
||||
return response
|
||||
|
||||
# Shorthand method decorators
|
||||
def get(self, uri, host=None, strict_slashes=False, version=None):
|
||||
def get(self, uri, host=None, strict_slashes=None, version=None,
|
||||
name=None):
|
||||
return self.route(uri, methods=frozenset({"GET"}), host=host,
|
||||
strict_slashes=strict_slashes, version=version)
|
||||
strict_slashes=strict_slashes, version=version,
|
||||
name=name)
|
||||
|
||||
def post(self, uri, host=None, strict_slashes=False, stream=False,
|
||||
version=None):
|
||||
def post(self, uri, host=None, strict_slashes=None, stream=False,
|
||||
version=None, name=None):
|
||||
return self.route(uri, methods=frozenset({"POST"}), host=host,
|
||||
strict_slashes=strict_slashes, stream=stream,
|
||||
version=version)
|
||||
version=version, name=name)
|
||||
|
||||
def put(self, uri, host=None, strict_slashes=False, stream=False,
|
||||
version=None):
|
||||
def put(self, uri, host=None, strict_slashes=None, stream=False,
|
||||
version=None, name=None):
|
||||
return self.route(uri, methods=frozenset({"PUT"}), host=host,
|
||||
strict_slashes=strict_slashes, stream=stream,
|
||||
version=version)
|
||||
version=version, name=name)
|
||||
|
||||
def head(self, uri, host=None, strict_slashes=False, version=None):
|
||||
def head(self, uri, host=None, strict_slashes=None, version=None,
|
||||
name=None):
|
||||
return self.route(uri, methods=frozenset({"HEAD"}), host=host,
|
||||
strict_slashes=strict_slashes, version=version)
|
||||
strict_slashes=strict_slashes, version=version,
|
||||
name=name)
|
||||
|
||||
def options(self, uri, host=None, strict_slashes=False, version=None):
|
||||
def options(self, uri, host=None, strict_slashes=None, version=None,
|
||||
name=None):
|
||||
return self.route(uri, methods=frozenset({"OPTIONS"}), host=host,
|
||||
strict_slashes=strict_slashes, version=version)
|
||||
strict_slashes=strict_slashes, version=version,
|
||||
name=name)
|
||||
|
||||
def patch(self, uri, host=None, strict_slashes=False, stream=False,
|
||||
version=None):
|
||||
def patch(self, uri, host=None, strict_slashes=None, stream=False,
|
||||
version=None, name=None):
|
||||
return self.route(uri, methods=frozenset({"PATCH"}), host=host,
|
||||
strict_slashes=strict_slashes, stream=stream,
|
||||
version=version)
|
||||
version=version, name=name)
|
||||
|
||||
def delete(self, uri, host=None, strict_slashes=False, version=None):
|
||||
def delete(self, uri, host=None, strict_slashes=None, version=None,
|
||||
name=None):
|
||||
return self.route(uri, methods=frozenset({"DELETE"}), host=host,
|
||||
strict_slashes=strict_slashes, version=version)
|
||||
strict_slashes=strict_slashes, version=version,
|
||||
name=name)
|
||||
|
||||
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None,
|
||||
strict_slashes=False, version=None):
|
||||
strict_slashes=None, version=None, name=None):
|
||||
"""A helper method to register class instance or
|
||||
functions as a handler to the application url
|
||||
routes.
|
||||
|
@ -188,6 +194,9 @@ class Sanic:
|
|||
:param methods: list or tuple of methods allowed, these are overridden
|
||||
if using a HTTPMethodView
|
||||
:param host:
|
||||
:param strict_slashes:
|
||||
:param version:
|
||||
:param name: user defined route name for url_for
|
||||
:return: function or class instance
|
||||
"""
|
||||
stream = False
|
||||
|
@ -210,15 +219,21 @@ class Sanic:
|
|||
stream = True
|
||||
break
|
||||
|
||||
if strict_slashes is None:
|
||||
strict_slashes = self.strict_slashes
|
||||
|
||||
self.route(uri=uri, methods=methods, host=host,
|
||||
strict_slashes=strict_slashes, stream=stream,
|
||||
version=version)(handler)
|
||||
version=version, name=name)(handler)
|
||||
return handler
|
||||
|
||||
# Decorator
|
||||
def websocket(self, uri, host=None, strict_slashes=False):
|
||||
def websocket(self, uri, host=None, strict_slashes=None,
|
||||
subprotocols=None, name=None):
|
||||
"""Decorate a function to be registered as a websocket route
|
||||
:param uri: path of the URL
|
||||
:param subprotocols: optional list of strings with the supported
|
||||
subprotocols
|
||||
:param host:
|
||||
:return: decorated function
|
||||
"""
|
||||
|
@ -229,6 +244,9 @@ class Sanic:
|
|||
if not uri.startswith('/'):
|
||||
uri = '/' + uri
|
||||
|
||||
if strict_slashes is None:
|
||||
strict_slashes = self.strict_slashes
|
||||
|
||||
def response(handler):
|
||||
async def websocket_handler(request, *args, **kwargs):
|
||||
request.app = self
|
||||
|
@ -238,13 +256,13 @@ class Sanic:
|
|||
# On Python3.5 the Transport classes in asyncio do not
|
||||
# have a get_protocol() method as in uvloop
|
||||
protocol = request.transport._protocol
|
||||
ws = await protocol.websocket_handshake(request)
|
||||
ws = await protocol.websocket_handshake(request, subprotocols)
|
||||
|
||||
# schedule the application handler
|
||||
# its future is kept in self.websocket_tasks in case it
|
||||
# needs to be cancelled due to the server being stopped
|
||||
fut = ensure_future(handler(request, ws, *args, **kwargs))
|
||||
self.websocket_tasks.append(fut)
|
||||
self.websocket_tasks.add(fut)
|
||||
try:
|
||||
await fut
|
||||
except (CancelledError, ConnectionClosed):
|
||||
|
@ -254,16 +272,19 @@ class Sanic:
|
|||
|
||||
self.router.add(uri=uri, handler=websocket_handler,
|
||||
methods=frozenset({'GET'}), host=host,
|
||||
strict_slashes=strict_slashes)
|
||||
strict_slashes=strict_slashes, name=name)
|
||||
return handler
|
||||
|
||||
return response
|
||||
|
||||
def add_websocket_route(self, handler, uri, host=None,
|
||||
strict_slashes=False):
|
||||
strict_slashes=None, name=None):
|
||||
"""A helper method to register a function as a websocket route."""
|
||||
return self.websocket(uri, host=host,
|
||||
strict_slashes=strict_slashes)(handler)
|
||||
if strict_slashes is None:
|
||||
strict_slashes = self.strict_slashes
|
||||
|
||||
return self.websocket(uri, host=host, strict_slashes=strict_slashes,
|
||||
name=name)(handler)
|
||||
|
||||
def enable_websocket(self, enable=True):
|
||||
"""Enable or disable the support for websocket.
|
||||
|
@ -327,13 +348,14 @@ class Sanic:
|
|||
# Static Files
|
||||
def static(self, uri, file_or_directory, pattern=r'/?.+',
|
||||
use_modified_since=True, use_content_range=False,
|
||||
stream_large_files=False):
|
||||
stream_large_files=False, name='static', host=None,
|
||||
strict_slashes=None):
|
||||
"""Register a root to serve files from. The input can either be a
|
||||
file or a directory. See
|
||||
"""
|
||||
static_register(self, uri, file_or_directory, pattern,
|
||||
use_modified_since, use_content_range,
|
||||
stream_large_files)
|
||||
stream_large_files, name, host, strict_slashes)
|
||||
|
||||
def blueprint(self, blueprint, **options):
|
||||
"""Register a blueprint on the application.
|
||||
|
@ -383,12 +405,31 @@ class Sanic:
|
|||
URLBuildError
|
||||
"""
|
||||
# find the route by the supplied view name
|
||||
uri, route = self.router.find_route_by_view_name(view_name)
|
||||
kw = {}
|
||||
# special static files url_for
|
||||
if view_name == 'static':
|
||||
kw.update(name=kwargs.pop('name', 'static'))
|
||||
elif view_name.endswith('.static'): # blueprint.static
|
||||
kwargs.pop('name', None)
|
||||
kw.update(name=view_name)
|
||||
|
||||
if not uri or not route:
|
||||
raise URLBuildError(
|
||||
'Endpoint with name `{}` was not found'.format(
|
||||
view_name))
|
||||
uri, route = self.router.find_route_by_view_name(view_name, **kw)
|
||||
if not (uri and route):
|
||||
raise URLBuildError('Endpoint with name `{}` was not found'.format(
|
||||
view_name))
|
||||
|
||||
if view_name == 'static' or view_name.endswith('.static'):
|
||||
filename = kwargs.pop('filename', None)
|
||||
# it's static folder
|
||||
if '<file_uri:' in uri:
|
||||
folder_ = uri.split('<file_uri:', 1)[0]
|
||||
if folder_.endswith('/'):
|
||||
folder_ = folder_[:-1]
|
||||
|
||||
if filename.startswith('/'):
|
||||
filename = filename[1:]
|
||||
|
||||
uri = '{}/{}'.format(folder_, filename)
|
||||
|
||||
if uri != '/' and uri.endswith('/'):
|
||||
uri = uri[:-1]
|
||||
|
@ -412,6 +453,16 @@ class Sanic:
|
|||
if netloc is None and external:
|
||||
netloc = self.config.get('SERVER_NAME', '')
|
||||
|
||||
if external:
|
||||
if not scheme:
|
||||
if ':' in netloc[:8]:
|
||||
scheme = netloc[:8].split(':', 1)[0]
|
||||
else:
|
||||
scheme = 'http'
|
||||
|
||||
if '://' in netloc[:8]:
|
||||
netloc = netloc.split('://', 1)[-1]
|
||||
|
||||
for match in matched_params:
|
||||
name, _type, pattern = self.router.parse_parameter_string(
|
||||
match)
|
||||
|
@ -493,6 +544,7 @@ class Sanic:
|
|||
|
||||
# Fetch handler from router
|
||||
handler, args, kwargs, uri = self.router.get(request)
|
||||
|
||||
request.uri_template = uri
|
||||
if handler is None:
|
||||
raise ServerError(
|
||||
|
@ -527,9 +579,9 @@ class Sanic:
|
|||
try:
|
||||
response = await self._run_response_middleware(request,
|
||||
response)
|
||||
except:
|
||||
log.exception(
|
||||
'Exception occured in one of response middleware handlers'
|
||||
except BaseException:
|
||||
error_logger.exception(
|
||||
'Exception occurred in one of response middleware handlers'
|
||||
)
|
||||
|
||||
# pass the response to the correct callback
|
||||
|
@ -553,7 +605,7 @@ class Sanic:
|
|||
def run(self, host=None, port=None, debug=False, ssl=None,
|
||||
sock=None, workers=1, protocol=None,
|
||||
backlog=100, stop_event=None, register_sys_signals=True,
|
||||
log_config=None):
|
||||
access_log=True):
|
||||
"""Run the HTTP Server and listen until keyboard interrupt or term
|
||||
signal. On termination, drain connections before closing.
|
||||
|
||||
|
@ -571,12 +623,10 @@ class Sanic:
|
|||
:param protocol: Subclass of asyncio protocol class
|
||||
:return: Nothing
|
||||
"""
|
||||
|
||||
if sock is None:
|
||||
host, port = host or "127.0.0.1", port or 8000
|
||||
|
||||
if log_config:
|
||||
self.log_config = log_config
|
||||
logging.config.dictConfig(log_config)
|
||||
if protocol is None:
|
||||
protocol = (WebSocketProtocol if self.websocket_enabled
|
||||
else HttpProtocol)
|
||||
|
@ -589,7 +639,7 @@ class Sanic:
|
|||
host=host, port=port, debug=debug, ssl=ssl, sock=sock,
|
||||
workers=workers, protocol=protocol, backlog=backlog,
|
||||
register_sys_signals=register_sys_signals,
|
||||
has_log=self.log_config is not None)
|
||||
access_log=access_log)
|
||||
|
||||
try:
|
||||
self.is_running = True
|
||||
|
@ -597,13 +647,13 @@ class Sanic:
|
|||
serve(**server_settings)
|
||||
else:
|
||||
serve_multiple(server_settings, workers)
|
||||
except:
|
||||
log.exception(
|
||||
except BaseException:
|
||||
error_logger.exception(
|
||||
'Experienced exception while trying to serve')
|
||||
raise
|
||||
finally:
|
||||
self.is_running = False
|
||||
log.info("Server Stopped")
|
||||
logger.info("Server Stopped")
|
||||
|
||||
def stop(self):
|
||||
"""This kills the Sanic"""
|
||||
|
@ -616,17 +666,16 @@ class Sanic:
|
|||
async def create_server(self, host=None, port=None, debug=False,
|
||||
ssl=None, sock=None, protocol=None,
|
||||
backlog=100, stop_event=None,
|
||||
log_config=LOGGING):
|
||||
access_log=True):
|
||||
"""Asynchronous version of `run`.
|
||||
|
||||
NOTE: This does not support multiprocessing and is not the preferred
|
||||
way to run a Sanic application.
|
||||
"""
|
||||
|
||||
if sock is None:
|
||||
host, port = host or "127.0.0.1", port or 8000
|
||||
|
||||
if log_config:
|
||||
logging.config.dictConfig(log_config)
|
||||
if protocol is None:
|
||||
protocol = (WebSocketProtocol if self.websocket_enabled
|
||||
else HttpProtocol)
|
||||
|
@ -635,14 +684,31 @@ class Sanic:
|
|||
warnings.simplefilter('default')
|
||||
warnings.warn("stop_event will be removed from future versions.",
|
||||
DeprecationWarning)
|
||||
|
||||
server_settings = self._helper(
|
||||
host=host, port=port, debug=debug, ssl=ssl, sock=sock,
|
||||
loop=get_event_loop(), protocol=protocol,
|
||||
backlog=backlog, run_async=True,
|
||||
has_log=log_config is not None)
|
||||
access_log=access_log)
|
||||
|
||||
# Trigger before_start events
|
||||
await self.trigger_events(
|
||||
server_settings.get('before_start', []),
|
||||
server_settings.get('loop')
|
||||
)
|
||||
|
||||
return await serve(**server_settings)
|
||||
|
||||
async def trigger_events(self, events, loop):
|
||||
"""Trigger events (functions or async)
|
||||
:param events: one or more sync or async functions to execute
|
||||
:param loop: event loop
|
||||
"""
|
||||
for event in events:
|
||||
result = event(loop)
|
||||
if isawaitable(result):
|
||||
await result
|
||||
|
||||
async def _run_request_middleware(self, request):
|
||||
# The if improves speed. I don't know why
|
||||
if self.request_middleware:
|
||||
|
@ -668,7 +734,7 @@ class Sanic:
|
|||
def _helper(self, host=None, port=None, debug=False,
|
||||
ssl=None, sock=None, workers=1, loop=None,
|
||||
protocol=HttpProtocol, backlog=100, stop_event=None,
|
||||
register_sys_signals=True, run_async=False, has_log=True):
|
||||
register_sys_signals=True, run_async=False, access_log=True):
|
||||
"""Helper function used by `run` and `create_server`."""
|
||||
if isinstance(ssl, dict):
|
||||
# try common aliaseses
|
||||
|
@ -702,12 +768,14 @@ class Sanic:
|
|||
'request_handler': self.handle_request,
|
||||
'error_handler': self.error_handler,
|
||||
'request_timeout': self.config.REQUEST_TIMEOUT,
|
||||
'response_timeout': self.config.RESPONSE_TIMEOUT,
|
||||
'keep_alive_timeout': self.config.KEEP_ALIVE_TIMEOUT,
|
||||
'request_max_size': self.config.REQUEST_MAX_SIZE,
|
||||
'keep_alive': self.config.KEEP_ALIVE,
|
||||
'loop': loop,
|
||||
'register_sys_signals': register_sys_signals,
|
||||
'backlog': backlog,
|
||||
'has_log': has_log,
|
||||
'access_log': access_log,
|
||||
'websocket_max_size': self.config.WEBSOCKET_MAX_SIZE,
|
||||
'websocket_max_queue': self.config.WEBSOCKET_MAX_QUEUE,
|
||||
'graceful_shutdown_timeout': self.config.GRACEFUL_SHUTDOWN_TIMEOUT
|
||||
|
@ -730,10 +798,10 @@ class Sanic:
|
|||
listeners = [partial(listener, self) for listener in listeners]
|
||||
server_settings[settings_name] = listeners
|
||||
|
||||
if debug:
|
||||
log.setLevel(logging.DEBUG)
|
||||
if self.configure_logging and debug:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
if self.config.LOGO is not None:
|
||||
log.debug(self.config.LOGO)
|
||||
logger.debug(self.config.LOGO)
|
||||
|
||||
if run_async:
|
||||
server_settings['run_async'] = True
|
||||
|
@ -743,6 +811,6 @@ class Sanic:
|
|||
proto = "http"
|
||||
if ssl is not None:
|
||||
proto = "https"
|
||||
log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
|
||||
logger.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
|
||||
|
||||
return server_settings
|
||||
|
|
|
@ -5,7 +5,7 @@ from sanic.views import CompositionView
|
|||
|
||||
FutureRoute = namedtuple('Route',
|
||||
['handler', 'uri', 'methods', 'host',
|
||||
'strict_slashes', 'stream', 'version'])
|
||||
'strict_slashes', 'stream', 'version', 'name'])
|
||||
FutureListener = namedtuple('Listener', ['handler', 'uri', 'methods', 'host'])
|
||||
FutureMiddleware = namedtuple('Route', ['middleware', 'args', 'kwargs'])
|
||||
FutureException = namedtuple('Route', ['handler', 'args', 'kwargs'])
|
||||
|
@ -14,11 +14,16 @@ FutureStatic = namedtuple('Route',
|
|||
|
||||
|
||||
class Blueprint:
|
||||
def __init__(self, name, url_prefix=None, host=None, version=None):
|
||||
|
||||
def __init__(self, name,
|
||||
url_prefix=None,
|
||||
host=None, version=None,
|
||||
strict_slashes=False):
|
||||
"""Create a new blueprint
|
||||
|
||||
:param name: unique name of the blueprint
|
||||
:param url_prefix: URL to be prefixed before all route URLs
|
||||
:param strict_slashes: strict to trailing slash
|
||||
"""
|
||||
self.name = name
|
||||
self.url_prefix = url_prefix
|
||||
|
@ -31,6 +36,7 @@ class Blueprint:
|
|||
self.middlewares = []
|
||||
self.statics = []
|
||||
self.version = version
|
||||
self.strict_slashes = strict_slashes
|
||||
|
||||
def register(self, app, options):
|
||||
"""Register the blueprint to the sanic app."""
|
||||
|
@ -47,14 +53,14 @@ class Blueprint:
|
|||
|
||||
version = future.version or self.version
|
||||
|
||||
app.route(
|
||||
uri=uri[1:] if uri.startswith('//') else uri,
|
||||
methods=future.methods,
|
||||
host=future.host or self.host,
|
||||
strict_slashes=future.strict_slashes,
|
||||
stream=future.stream,
|
||||
version=version
|
||||
)(future.handler)
|
||||
app.route(uri=uri[1:] if uri.startswith('//') else uri,
|
||||
methods=future.methods,
|
||||
host=future.host or self.host,
|
||||
strict_slashes=future.strict_slashes,
|
||||
stream=future.stream,
|
||||
version=version,
|
||||
name=future.name,
|
||||
)(future.handler)
|
||||
|
||||
for future in self.websocket_routes:
|
||||
# attach the blueprint name to the handler so that it can be
|
||||
|
@ -62,11 +68,11 @@ class Blueprint:
|
|||
future.handler.__blueprintname__ = self.name
|
||||
# Prepend the blueprint URI prefix if available
|
||||
uri = url_prefix + future.uri if url_prefix else future.uri
|
||||
app.websocket(
|
||||
uri=uri,
|
||||
host=future.host or self.host,
|
||||
strict_slashes=future.strict_slashes
|
||||
)(future.handler)
|
||||
app.websocket(uri=uri,
|
||||
host=future.host or self.host,
|
||||
strict_slashes=future.strict_slashes,
|
||||
name=future.name,
|
||||
)(future.handler)
|
||||
|
||||
# Middleware
|
||||
for future in self.middlewares:
|
||||
|
@ -94,27 +100,35 @@ class Blueprint:
|
|||
app.listener(event)(listener)
|
||||
|
||||
def route(self, uri, methods=frozenset({'GET'}), host=None,
|
||||
strict_slashes=False, stream=False, version=None):
|
||||
strict_slashes=None, stream=False, version=None, name=None):
|
||||
"""Create a blueprint route from a decorated function.
|
||||
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
:param methods: list of acceptable HTTP methods.
|
||||
"""
|
||||
if strict_slashes is None:
|
||||
strict_slashes = self.strict_slashes
|
||||
|
||||
def decorator(handler):
|
||||
route = FutureRoute(
|
||||
handler, uri, methods, host, strict_slashes, stream, version)
|
||||
handler, uri, methods, host, strict_slashes, stream, version,
|
||||
name)
|
||||
self.routes.append(route)
|
||||
return handler
|
||||
return decorator
|
||||
|
||||
def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None,
|
||||
strict_slashes=False, version=None):
|
||||
strict_slashes=None, version=None, name=None):
|
||||
"""Create a blueprint route from a function.
|
||||
|
||||
:param handler: function for handling uri requests. Accepts function,
|
||||
or class instance with a view_class method.
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
:param methods: list of acceptable HTTP methods.
|
||||
:param host:
|
||||
:param strict_slashes:
|
||||
:param version:
|
||||
:param name: user defined route name for url_for
|
||||
:return: function or class instance
|
||||
"""
|
||||
# Handle HTTPMethodView differently
|
||||
|
@ -125,27 +139,36 @@ class Blueprint:
|
|||
if getattr(handler.view_class, method.lower(), None):
|
||||
methods.add(method)
|
||||
|
||||
if strict_slashes is None:
|
||||
strict_slashes = self.strict_slashes
|
||||
|
||||
# handle composition view differently
|
||||
if isinstance(handler, CompositionView):
|
||||
methods = handler.handlers.keys()
|
||||
|
||||
self.route(uri=uri, methods=methods, host=host,
|
||||
strict_slashes=strict_slashes, version=version)(handler)
|
||||
strict_slashes=strict_slashes, version=version,
|
||||
name=name)(handler)
|
||||
return handler
|
||||
|
||||
def websocket(self, uri, host=None, strict_slashes=False, version=None):
|
||||
def websocket(self, uri, host=None, strict_slashes=None, version=None,
|
||||
name=None):
|
||||
"""Create a blueprint websocket route from a decorated function.
|
||||
|
||||
:param uri: endpoint at which the route will be accessible.
|
||||
"""
|
||||
if strict_slashes is None:
|
||||
strict_slashes = self.strict_slashes
|
||||
|
||||
def decorator(handler):
|
||||
route = FutureRoute(handler, uri, [], host, strict_slashes,
|
||||
False, version)
|
||||
False, version, name)
|
||||
self.websocket_routes.append(route)
|
||||
return handler
|
||||
return decorator
|
||||
|
||||
def add_websocket_route(self, handler, uri, host=None, version=None):
|
||||
def add_websocket_route(self, handler, uri, host=None, version=None,
|
||||
name=None):
|
||||
"""Create a blueprint websocket route from a function.
|
||||
|
||||
:param handler: function for handling uri requests. Accepts function,
|
||||
|
@ -153,7 +176,7 @@ class Blueprint:
|
|||
:param uri: endpoint at which the route will be accessible.
|
||||
:return: function or class instance
|
||||
"""
|
||||
self.websocket(uri=uri, host=host, version=version)(handler)
|
||||
self.websocket(uri=uri, host=host, version=version, name=name)(handler)
|
||||
return handler
|
||||
|
||||
def listener(self, event):
|
||||
|
@ -195,40 +218,57 @@ class Blueprint:
|
|||
:param uri: endpoint at which the route will be accessible.
|
||||
:param file_or_directory: Static asset.
|
||||
"""
|
||||
name = kwargs.pop('name', 'static')
|
||||
if not name.startswith(self.name + '.'):
|
||||
name = '{}.{}'.format(self.name, name)
|
||||
kwargs.update(name=name)
|
||||
|
||||
strict_slashes = kwargs.get('strict_slashes')
|
||||
if strict_slashes is None and self.strict_slashes is not None:
|
||||
kwargs.update(strict_slashes=self.strict_slashes)
|
||||
|
||||
static = FutureStatic(uri, file_or_directory, args, kwargs)
|
||||
self.statics.append(static)
|
||||
|
||||
# Shorthand method decorators
|
||||
def get(self, uri, host=None, strict_slashes=False, version=None):
|
||||
def get(self, uri, host=None, strict_slashes=None, version=None,
|
||||
name=None):
|
||||
return self.route(uri, methods=["GET"], host=host,
|
||||
strict_slashes=strict_slashes, version=version)
|
||||
strict_slashes=strict_slashes, version=version,
|
||||
name=name)
|
||||
|
||||
def post(self, uri, host=None, strict_slashes=False, stream=False,
|
||||
version=None):
|
||||
def post(self, uri, host=None, strict_slashes=None, stream=False,
|
||||
version=None, name=None):
|
||||
return self.route(uri, methods=["POST"], host=host,
|
||||
strict_slashes=strict_slashes, stream=stream,
|
||||
version=version)
|
||||
version=version, name=name)
|
||||
|
||||
def put(self, uri, host=None, strict_slashes=False, stream=False,
|
||||
version=None):
|
||||
def put(self, uri, host=None, strict_slashes=None, stream=False,
|
||||
version=None, name=None):
|
||||
return self.route(uri, methods=["PUT"], host=host,
|
||||
strict_slashes=strict_slashes, stream=stream,
|
||||
version=version)
|
||||
version=version, name=name)
|
||||
|
||||
def head(self, uri, host=None, strict_slashes=False, version=None):
|
||||
def head(self, uri, host=None, strict_slashes=None, version=None,
|
||||
name=None):
|
||||
return self.route(uri, methods=["HEAD"], host=host,
|
||||
strict_slashes=strict_slashes, version=version)
|
||||
strict_slashes=strict_slashes, version=version,
|
||||
name=name)
|
||||
|
||||
def options(self, uri, host=None, strict_slashes=False, version=None):
|
||||
def options(self, uri, host=None, strict_slashes=None, version=None,
|
||||
name=None):
|
||||
return self.route(uri, methods=["OPTIONS"], host=host,
|
||||
strict_slashes=strict_slashes, version=version)
|
||||
strict_slashes=strict_slashes, version=version,
|
||||
name=name)
|
||||
|
||||
def patch(self, uri, host=None, strict_slashes=False, stream=False,
|
||||
version=None):
|
||||
def patch(self, uri, host=None, strict_slashes=None, stream=False,
|
||||
version=None, name=None):
|
||||
return self.route(uri, methods=["PATCH"], host=host,
|
||||
strict_slashes=strict_slashes, stream=stream,
|
||||
version=version)
|
||||
version=version, name=name)
|
||||
|
||||
def delete(self, uri, host=None, strict_slashes=False, version=None):
|
||||
def delete(self, uri, host=None, strict_slashes=None, version=None,
|
||||
name=None):
|
||||
return self.route(uri, methods=["DELETE"], host=host,
|
||||
strict_slashes=strict_slashes, version=version)
|
||||
strict_slashes=strict_slashes, version=version,
|
||||
name=name)
|
||||
|
|
107
sanic/config.py
107
sanic/config.py
|
@ -1,103 +1,9 @@
|
|||
import os
|
||||
import sys
|
||||
import syslog
|
||||
import platform
|
||||
import types
|
||||
|
||||
from sanic.log import DefaultFilter
|
||||
|
||||
SANIC_PREFIX = 'SANIC_'
|
||||
|
||||
_address_dict = {
|
||||
'Windows': ('localhost', 514),
|
||||
'Darwin': '/var/run/syslog',
|
||||
'Linux': '/dev/log',
|
||||
'FreeBSD': '/var/run/log'
|
||||
}
|
||||
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'filters': {
|
||||
'accessFilter': {
|
||||
'()': DefaultFilter,
|
||||
'param': [0, 10, 20]
|
||||
},
|
||||
'errorFilter': {
|
||||
'()': DefaultFilter,
|
||||
'param': [30, 40, 50]
|
||||
}
|
||||
},
|
||||
'formatters': {
|
||||
'simple': {
|
||||
'format': '%(asctime)s - (%(name)s)[%(levelname)s]: %(message)s',
|
||||
'datefmt': '%Y-%m-%d %H:%M:%S'
|
||||
},
|
||||
'access': {
|
||||
'format': '%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: ' +
|
||||
'%(request)s %(message)s %(status)d %(byte)d',
|
||||
'datefmt': '%Y-%m-%d %H:%M:%S'
|
||||
}
|
||||
},
|
||||
'handlers': {
|
||||
'internal': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'filters': ['accessFilter'],
|
||||
'formatter': 'simple',
|
||||
'stream': sys.stderr
|
||||
},
|
||||
'accessStream': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'filters': ['accessFilter'],
|
||||
'formatter': 'access',
|
||||
'stream': sys.stderr
|
||||
},
|
||||
'errorStream': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'filters': ['errorFilter'],
|
||||
'formatter': 'simple',
|
||||
'stream': sys.stderr
|
||||
},
|
||||
# before you use accessSysLog, be sure that log levels
|
||||
# 0, 10, 20 have been enabled in you syslog configuration
|
||||
# otherwise you won't be able to see the output in syslog
|
||||
# logging file.
|
||||
'accessSysLog': {
|
||||
'class': 'logging.handlers.SysLogHandler',
|
||||
'address': _address_dict.get(platform.system(),
|
||||
('localhost', 514)),
|
||||
'facility': syslog.LOG_DAEMON,
|
||||
'filters': ['accessFilter'],
|
||||
'formatter': 'access'
|
||||
},
|
||||
'errorSysLog': {
|
||||
'class': 'logging.handlers.SysLogHandler',
|
||||
'address': _address_dict.get(platform.system(),
|
||||
('localhost', 514)),
|
||||
'facility': syslog.LOG_DAEMON,
|
||||
'filters': ['errorFilter'],
|
||||
'formatter': 'simple'
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'sanic': {
|
||||
'level': 'DEBUG',
|
||||
'handlers': ['internal', 'errorStream']
|
||||
},
|
||||
'network': {
|
||||
'level': 'DEBUG',
|
||||
'handlers': ['accessStream', 'errorStream']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# this happens when using container or systems without syslog
|
||||
# keep things in config would cause file not exists error
|
||||
_addr = LOGGING['handlers']['accessSysLog']['address']
|
||||
if type(_addr) is str and not os.path.exists(_addr):
|
||||
LOGGING['handlers'].pop('accessSysLog')
|
||||
LOGGING['handlers'].pop('errorSysLog')
|
||||
|
||||
|
||||
class Config(dict):
|
||||
def __init__(self, defaults=None, load_env=True, keep_alive=True):
|
||||
|
@ -125,13 +31,16 @@ class Config(dict):
|
|||
"""
|
||||
self.REQUEST_MAX_SIZE = 100000000 # 100 megabytes
|
||||
self.REQUEST_TIMEOUT = 60 # 60 seconds
|
||||
self.RESPONSE_TIMEOUT = 60 # 60 seconds
|
||||
self.KEEP_ALIVE = keep_alive
|
||||
self.KEEP_ALIVE_TIMEOUT = 5 # 5 seconds
|
||||
self.WEBSOCKET_MAX_SIZE = 2 ** 20 # 1 megabytes
|
||||
self.WEBSOCKET_MAX_QUEUE = 32
|
||||
self.GRACEFUL_SHUTDOWN_TIMEOUT = 15.0 # 15 sec
|
||||
|
||||
if load_env:
|
||||
self.load_environment_vars()
|
||||
prefix = SANIC_PREFIX if load_env is True else load_env
|
||||
self.load_environment_vars(prefix=prefix)
|
||||
|
||||
def __getattr__(self, attr):
|
||||
try:
|
||||
|
@ -195,14 +104,14 @@ class Config(dict):
|
|||
if key.isupper():
|
||||
self[key] = getattr(obj, key)
|
||||
|
||||
def load_environment_vars(self):
|
||||
def load_environment_vars(self, prefix=SANIC_PREFIX):
|
||||
"""
|
||||
Looks for any ``SANIC_`` prefixed environment variables and applies
|
||||
Looks for prefixed environment variables and applies
|
||||
them to the configuration if present.
|
||||
"""
|
||||
for k, v in os.environ.items():
|
||||
if k.startswith(SANIC_PREFIX):
|
||||
_, config_key = k.split(SANIC_PREFIX, 1)
|
||||
if k.startswith(prefix):
|
||||
_, config_key = k.split(prefix, 1)
|
||||
try:
|
||||
self[config_key] = int(v)
|
||||
except ValueError:
|
||||
|
|
|
@ -98,7 +98,8 @@ class Cookie(dict):
|
|||
def __setitem__(self, key, value):
|
||||
if key not in self._keys:
|
||||
raise KeyError("Unknown cookie property")
|
||||
return super().__setitem__(key, value)
|
||||
if value is not False:
|
||||
return super().__setitem__(key, value)
|
||||
|
||||
def encode(self, encoding):
|
||||
output = ['%s=%s' % (self.key, _quote(self.value))]
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from sanic.response import ALL_STATUS_CODES, COMMON_STATUS_CODES
|
||||
from sanic.response import STATUS_CODES
|
||||
|
||||
TRACEBACK_STYLE = '''
|
||||
<style>
|
||||
|
@ -150,18 +150,33 @@ class InvalidUsage(SanicException):
|
|||
pass
|
||||
|
||||
|
||||
@add_status_code(405)
|
||||
class MethodNotSupported(SanicException):
|
||||
def __init__(self, message, method, allowed_methods):
|
||||
super().__init__(message)
|
||||
self.headers = dict()
|
||||
self.headers["Allow"] = ", ".join(allowed_methods)
|
||||
if method in ['HEAD', 'PATCH', 'PUT', 'DELETE']:
|
||||
self.headers['Content-Length'] = 0
|
||||
|
||||
|
||||
@add_status_code(500)
|
||||
class ServerError(SanicException):
|
||||
pass
|
||||
|
||||
|
||||
@add_status_code(503)
|
||||
class ServiceUnavailable(SanicException):
|
||||
"""The server is currently unavailable (because it is overloaded or
|
||||
down for maintenance). Generally, this is a temporary state."""
|
||||
pass
|
||||
|
||||
|
||||
class URLBuildError(ServerError):
|
||||
pass
|
||||
|
||||
|
||||
class FileNotFound(NotFound):
|
||||
pass
|
||||
|
||||
def __init__(self, message, path, relative_url):
|
||||
super().__init__(message)
|
||||
self.path = path
|
||||
|
@ -170,6 +185,13 @@ class FileNotFound(NotFound):
|
|||
|
||||
@add_status_code(408)
|
||||
class RequestTimeout(SanicException):
|
||||
"""The Web server (running the Web site) thinks that there has been too
|
||||
long an interval of time between 1) the establishment of an IP
|
||||
connection (socket) between the client and the server and
|
||||
2) the receipt of any data on that socket, so the server has dropped
|
||||
the connection. The socket connection has actually been lost - the Web
|
||||
server has 'timed out' on that particular socket connection.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
@ -184,8 +206,6 @@ class HeaderNotFound(InvalidUsage):
|
|||
|
||||
@add_status_code(416)
|
||||
class ContentRangeError(SanicException):
|
||||
pass
|
||||
|
||||
def __init__(self, message, content_range):
|
||||
super().__init__(message)
|
||||
self.headers = {
|
||||
|
@ -208,45 +228,47 @@ class Unauthorized(SanicException):
|
|||
"""
|
||||
Unauthorized exception (401 HTTP status code).
|
||||
|
||||
:param message: Message describing the exception.
|
||||
:param status_code: HTTP Status code.
|
||||
:param scheme: Name of the authentication scheme to be used.
|
||||
:param challenge: A dict containing values to add to the WWW-Authenticate
|
||||
header that is generated. This is especially useful when dealing with
|
||||
the Digest scheme. (optional)
|
||||
|
||||
When present, kwargs is used to complete the WWW-Authentication header.
|
||||
|
||||
Examples::
|
||||
|
||||
# With a Basic auth-scheme, realm MUST be present:
|
||||
challenge = {"realm": "Restricted Area"}
|
||||
raise Unauthorized("Auth required.", "Basic", challenge)
|
||||
raise Unauthorized("Auth required.",
|
||||
scheme="Basic",
|
||||
realm="Restricted Area")
|
||||
|
||||
# With a Digest auth-scheme, things are a bit more complicated:
|
||||
challenge = {
|
||||
"realm": "Restricted Area",
|
||||
"qop": "auth, auth-int",
|
||||
"algorithm": "MD5",
|
||||
"nonce": "abcdef",
|
||||
"opaque": "zyxwvu"
|
||||
}
|
||||
raise Unauthorized("Auth required.", "Digest", challenge)
|
||||
raise Unauthorized("Auth required.",
|
||||
scheme="Digest",
|
||||
realm="Restricted Area",
|
||||
qop="auth, auth-int",
|
||||
algorithm="MD5",
|
||||
nonce="abcdef",
|
||||
opaque="zyxwvu")
|
||||
|
||||
# With a Bearer auth-scheme, realm is optional:
|
||||
challenge = {"realm": "Restricted Area"}
|
||||
raise Unauthorized("Auth required.", "Bearer", challenge)
|
||||
# With a Bearer auth-scheme, realm is optional so you can write:
|
||||
raise Unauthorized("Auth required.", scheme="Bearer")
|
||||
|
||||
# or, if you want to specify the realm:
|
||||
raise Unauthorized("Auth required.",
|
||||
scheme="Bearer",
|
||||
realm="Restricted Area")
|
||||
"""
|
||||
pass
|
||||
def __init__(self, message, status_code=None, scheme=None, **kwargs):
|
||||
super().__init__(message, status_code)
|
||||
|
||||
def __init__(self, message, scheme, challenge=None):
|
||||
super().__init__(message)
|
||||
# if auth-scheme is specified, set "WWW-Authenticate" header
|
||||
if scheme is not None:
|
||||
values = ["{!s}={!r}".format(k, v) for k, v in kwargs.items()]
|
||||
challenge = ', '.join(values)
|
||||
|
||||
chal = ""
|
||||
|
||||
if challenge is not None:
|
||||
values = ["{!s}={!r}".format(k, v) for k, v in challenge.items()]
|
||||
chal = ', '.join(values)
|
||||
|
||||
self.headers = {
|
||||
"WWW-Authenticate": "{} {}".format(scheme, chal).rstrip()
|
||||
}
|
||||
self.headers = {
|
||||
"WWW-Authenticate": "{} {}".format(scheme, challenge).rstrip()
|
||||
}
|
||||
|
||||
|
||||
def abort(status_code, message=None):
|
||||
|
@ -259,8 +281,7 @@ def abort(status_code, message=None):
|
|||
in response.py for the given status code.
|
||||
"""
|
||||
if message is None:
|
||||
message = COMMON_STATUS_CODES.get(status_code,
|
||||
ALL_STATUS_CODES.get(status_code))
|
||||
message = STATUS_CODES.get(status_code)
|
||||
# These are stored as bytes in the STATUS_CODES dict
|
||||
message = message.decode('utf8')
|
||||
sanic_exception = _sanic_exceptions.get(status_code, SanicException)
|
||||
|
|
|
@ -12,7 +12,7 @@ from sanic.exceptions import (
|
|||
TRACEBACK_WRAPPER_HTML,
|
||||
TRACEBACK_WRAPPER_INNER_HTML,
|
||||
TRACEBACK_BORDER)
|
||||
from sanic.log import log
|
||||
from sanic.log import logger
|
||||
from sanic.response import text, html
|
||||
|
||||
|
||||
|
@ -86,12 +86,13 @@ class ErrorHandler:
|
|||
self.log(format_exc())
|
||||
if self.debug:
|
||||
url = getattr(request, 'url', 'unknown')
|
||||
response_message = (
|
||||
'Exception raised in exception handler "{}" '
|
||||
'for uri: "{}"\n{}').format(
|
||||
handler.__name__, url, format_exc())
|
||||
log.error(response_message)
|
||||
return text(response_message, 500)
|
||||
response_message = ('Exception raised in exception handler '
|
||||
'"%s" for uri: "%s"\n%s')
|
||||
logger.error(response_message,
|
||||
handler.__name__, url, format_exc())
|
||||
|
||||
return text(response_message % (
|
||||
handler.__name__, url, format_exc()), 500)
|
||||
else:
|
||||
return text('An error occurred while handling an error', 500)
|
||||
return response
|
||||
|
@ -101,7 +102,7 @@ class ErrorHandler:
|
|||
Override this method in an ErrorHandler subclass to prevent
|
||||
logging exceptions.
|
||||
"""
|
||||
getattr(log, level)(message)
|
||||
getattr(logger, level)(message)
|
||||
|
||||
def default(self, request, exception):
|
||||
self.log(format_exc())
|
||||
|
@ -114,10 +115,9 @@ class ErrorHandler:
|
|||
elif self.debug:
|
||||
html_output = self._render_traceback_html(exception, request)
|
||||
|
||||
response_message = (
|
||||
'Exception occurred while handling uri: "{}"\n{}'.format(
|
||||
request.url, format_exc()))
|
||||
log.error(response_message)
|
||||
response_message = ('Exception occurred while handling uri: '
|
||||
'"%s"\n%s')
|
||||
logger.error(response_message, request.url, format_exc())
|
||||
return html(html_output, status=500)
|
||||
else:
|
||||
return html(INTERNAL_SERVER_ERROR_HTML, status=500)
|
||||
|
|
67
sanic/log.py
67
sanic/log.py
|
@ -1,18 +1,63 @@
|
|||
import logging
|
||||
import sys
|
||||
|
||||
|
||||
class DefaultFilter(logging.Filter):
|
||||
LOGGING_CONFIG_DEFAULTS = dict(
|
||||
version=1,
|
||||
disable_existing_loggers=False,
|
||||
|
||||
def __init__(self, param=None):
|
||||
self.param = param
|
||||
loggers={
|
||||
"root": {
|
||||
"level": "INFO",
|
||||
"handlers": ["console"]
|
||||
},
|
||||
"sanic.error": {
|
||||
"level": "INFO",
|
||||
"handlers": ["error_console"],
|
||||
"propagate": True,
|
||||
"qualname": "sanic.error"
|
||||
},
|
||||
|
||||
def filter(self, record):
|
||||
if self.param is None:
|
||||
return True
|
||||
if record.levelno in self.param:
|
||||
return True
|
||||
return False
|
||||
"sanic.access": {
|
||||
"level": "INFO",
|
||||
"handlers": ["access_console"],
|
||||
"propagate": True,
|
||||
"qualname": "sanic.access"
|
||||
}
|
||||
},
|
||||
handlers={
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "generic",
|
||||
"stream": sys.stdout
|
||||
},
|
||||
"error_console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "generic",
|
||||
"stream": sys.stderr
|
||||
},
|
||||
"access_console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "access",
|
||||
"stream": sys.stdout
|
||||
},
|
||||
},
|
||||
formatters={
|
||||
"generic": {
|
||||
"format": "%(asctime)s [%(process)d] [%(levelname)s] %(message)s",
|
||||
"datefmt": "[%Y-%m-%d %H:%M:%S %z]",
|
||||
"class": "logging.Formatter"
|
||||
},
|
||||
"access": {
|
||||
"format": "%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: " +
|
||||
"%(request)s %(message)s %(status)d %(byte)d",
|
||||
"datefmt": "[%Y-%m-%d %H:%M:%S %z]",
|
||||
"class": "logging.Formatter"
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
log = logging.getLogger('sanic')
|
||||
netlog = logging.getLogger('network')
|
||||
logger = logging.getLogger('root')
|
||||
error_logger = logging.getLogger('sanic.error')
|
||||
access_logger = logging.getLogger('sanic.access')
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import sys
|
||||
import json
|
||||
import socket
|
||||
from cgi import parse_header
|
||||
from collections import namedtuple
|
||||
from http.cookies import SimpleCookie
|
||||
|
@ -17,10 +18,11 @@ except ImportError:
|
|||
json_loads = json.loads
|
||||
|
||||
from sanic.exceptions import InvalidUsage
|
||||
from sanic.log import log
|
||||
|
||||
from sanic.log import error_logger
|
||||
|
||||
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"
|
||||
|
@ -45,7 +47,8 @@ class Request(dict):
|
|||
__slots__ = (
|
||||
'app', 'headers', 'version', 'method', '_cookies', 'transport',
|
||||
'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
|
||||
'_ip', '_parsed_url', 'uri_template', 'stream', '_remote_addr'
|
||||
'_ip', '_parsed_url', 'uri_template', 'stream', '_remote_addr',
|
||||
'_socket', '_port'
|
||||
)
|
||||
|
||||
def __init__(self, url_bytes, headers, version, method, transport):
|
||||
|
@ -68,15 +71,27 @@ class Request(dict):
|
|||
self._cookies = None
|
||||
self.stream = None
|
||||
|
||||
def __repr__(self):
|
||||
if self.method is None or not self.path:
|
||||
return '<{0}>'.format(self.__class__.__name__)
|
||||
return '<{0}: {1} {2}>'.format(self.__class__.__name__,
|
||||
self.method,
|
||||
self.path)
|
||||
|
||||
@property
|
||||
def json(self):
|
||||
if self.parsed_json is None:
|
||||
try:
|
||||
self.parsed_json = json_loads(self.body)
|
||||
except Exception:
|
||||
if not self.body:
|
||||
return None
|
||||
raise InvalidUsage("Failed when parsing body as json")
|
||||
self.load_json()
|
||||
|
||||
return self.parsed_json
|
||||
|
||||
def load_json(self, loads=json_loads):
|
||||
try:
|
||||
self.parsed_json = loads(self.body)
|
||||
except Exception:
|
||||
if not self.body:
|
||||
return None
|
||||
raise InvalidUsage("Failed when parsing body as json")
|
||||
|
||||
return self.parsed_json
|
||||
|
||||
|
@ -114,7 +129,7 @@ class Request(dict):
|
|||
self.parsed_form, self.parsed_files = (
|
||||
parse_multipart_form(self.body, boundary))
|
||||
except Exception:
|
||||
log.exception("Failed when parsing form")
|
||||
error_logger.exception("Failed when parsing form")
|
||||
|
||||
return self.parsed_form
|
||||
|
||||
|
@ -154,11 +169,36 @@ class Request(dict):
|
|||
|
||||
@property
|
||||
def ip(self):
|
||||
if not hasattr(self, '_ip'):
|
||||
self._ip = (self.transport.get_extra_info('peername') or
|
||||
(None, None))
|
||||
if not hasattr(self, '_socket'):
|
||||
self._get_address()
|
||||
return self._ip
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
if not hasattr(self, '_socket'):
|
||||
self._get_address()
|
||||
return self._port
|
||||
|
||||
@property
|
||||
def socket(self):
|
||||
if not hasattr(self, '_socket'):
|
||||
self._get_socket()
|
||||
return self._socket
|
||||
|
||||
def _get_address(self):
|
||||
sock = self.transport.get_extra_info('socket')
|
||||
|
||||
if sock.family == socket.AF_INET:
|
||||
self._socket = (self.transport.get_extra_info('peername') or
|
||||
(None, None))
|
||||
self._ip, self._port = self._socket
|
||||
elif sock.family == socket.AF_INET6:
|
||||
self._socket = (self.transport.get_extra_info('peername') or
|
||||
(None, None, None, None))
|
||||
self._ip, self._port, *_ = self._socket
|
||||
else:
|
||||
self._ip, self._port = (None, None)
|
||||
|
||||
@property
|
||||
def remote_addr(self):
|
||||
"""Attempt to return the original client ip based on X-Forwarded-For.
|
||||
|
@ -170,8 +210,8 @@ class Request(dict):
|
|||
remote_addrs = [
|
||||
addr for addr in [
|
||||
addr.strip() for addr in forwarded_for
|
||||
] if addr
|
||||
]
|
||||
] if addr
|
||||
]
|
||||
if len(remote_addrs) > 0:
|
||||
self._remote_addr = remote_addrs[0]
|
||||
else:
|
||||
|
|
|
@ -3,20 +3,14 @@ from os import path
|
|||
|
||||
try:
|
||||
from ujson import dumps as json_dumps
|
||||
except:
|
||||
except BaseException:
|
||||
from json import dumps as json_dumps
|
||||
|
||||
from aiofiles import open as open_async
|
||||
|
||||
from sanic.cookies import CookieJar
|
||||
|
||||
COMMON_STATUS_CODES = {
|
||||
200: b'OK',
|
||||
400: b'Bad Request',
|
||||
404: b'Not Found',
|
||||
500: b'Internal Server Error',
|
||||
}
|
||||
ALL_STATUS_CODES = {
|
||||
STATUS_CODES = {
|
||||
100: b'Continue',
|
||||
101: b'Switching Protocols',
|
||||
102: b'Processing',
|
||||
|
@ -56,6 +50,7 @@ ALL_STATUS_CODES = {
|
|||
415: b'Unsupported Media Type',
|
||||
416: b'Requested Range Not Satisfiable',
|
||||
417: b'Expectation Failed',
|
||||
418: b'I\'m a teapot',
|
||||
422: b'Unprocessable Entity',
|
||||
423: b'Locked',
|
||||
424: b'Failed Dependency',
|
||||
|
@ -63,6 +58,7 @@ ALL_STATUS_CODES = {
|
|||
428: b'Precondition Required',
|
||||
429: b'Too Many Requests',
|
||||
431: b'Request Header Fields Too Large',
|
||||
451: b'Unavailable For Legal Reasons',
|
||||
500: b'Internal Server Error',
|
||||
501: b'Not Implemented',
|
||||
502: b'Bad Gateway',
|
||||
|
@ -109,8 +105,9 @@ class BaseHTTPResponse:
|
|||
|
||||
class StreamingHTTPResponse(BaseHTTPResponse):
|
||||
__slots__ = (
|
||||
'transport', 'streaming_fn',
|
||||
'status', 'content_type', 'headers', '_cookies')
|
||||
'transport', 'streaming_fn', 'status',
|
||||
'content_type', 'headers', '_cookies'
|
||||
)
|
||||
|
||||
def __init__(self, streaming_fn, status=200, headers=None,
|
||||
content_type='text/plain'):
|
||||
|
@ -159,11 +156,10 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
|||
|
||||
headers = self._parse_headers()
|
||||
|
||||
# Try to pull from the common codes first
|
||||
# Speeds up response rate 6% over pulling from all
|
||||
status = COMMON_STATUS_CODES.get(self.status)
|
||||
if not status:
|
||||
status = ALL_STATUS_CODES.get(self.status)
|
||||
if self.status is 200:
|
||||
status = b'OK'
|
||||
else:
|
||||
status = STATUS_CODES.get(self.status)
|
||||
|
||||
return (b'HTTP/%b %d %b\r\n'
|
||||
b'%b'
|
||||
|
@ -206,11 +202,10 @@ class HTTPResponse(BaseHTTPResponse):
|
|||
|
||||
headers = self._parse_headers()
|
||||
|
||||
# Try to pull from the common codes first
|
||||
# Speeds up response rate 6% over pulling from all
|
||||
status = COMMON_STATUS_CODES.get(self.status)
|
||||
if not status:
|
||||
status = ALL_STATUS_CODES.get(self.status, b'UNKNOWN RESPONSE')
|
||||
if self.status is 200:
|
||||
status = b'OK'
|
||||
else:
|
||||
status = STATUS_CODES.get(self.status, b'UNKNOWN RESPONSE')
|
||||
|
||||
return (b'HTTP/%b %d %b\r\n'
|
||||
b'Connection: %b\r\n'
|
||||
|
@ -234,7 +229,8 @@ class HTTPResponse(BaseHTTPResponse):
|
|||
|
||||
|
||||
def json(body, status=200, headers=None,
|
||||
content_type="application/json", **kwargs):
|
||||
content_type="application/json", dumps=json_dumps,
|
||||
**kwargs):
|
||||
"""
|
||||
Returns response object with body in json format.
|
||||
|
||||
|
@ -243,7 +239,7 @@ def json(body, status=200, headers=None,
|
|||
:param headers: Custom Headers.
|
||||
:param kwargs: Remaining arguments that are passed to the json encoder.
|
||||
"""
|
||||
return HTTPResponse(json_dumps(body, **kwargs), headers=headers,
|
||||
return HTTPResponse(dumps(body, **kwargs), headers=headers,
|
||||
status=status, content_type=content_type)
|
||||
|
||||
|
||||
|
@ -288,15 +284,22 @@ def html(body, status=200, headers=None):
|
|||
content_type="text/html; charset=utf-8")
|
||||
|
||||
|
||||
async def file(location, mime_type=None, headers=None, _range=None):
|
||||
async def file(
|
||||
location, mime_type=None, headers=None, filename=None, _range=None):
|
||||
"""Return a response object with file data.
|
||||
|
||||
:param location: Location of file on system.
|
||||
:param mime_type: Specific mime_type.
|
||||
:param headers: Custom Headers.
|
||||
:param filename: Override filename.
|
||||
:param _range:
|
||||
"""
|
||||
filename = path.split(location)[-1]
|
||||
headers = headers or {}
|
||||
if filename:
|
||||
headers.setdefault(
|
||||
'Content-Disposition',
|
||||
'attachment; filename="{}"'.format(filename))
|
||||
filename = filename or path.split(location)[-1]
|
||||
|
||||
async with open_async(location, mode='rb') as _file:
|
||||
if _range:
|
||||
|
@ -308,24 +311,30 @@ async def file(location, mime_type=None, headers=None, _range=None):
|
|||
out_stream = await _file.read()
|
||||
|
||||
mime_type = mime_type or guess_type(filename)[0] or 'text/plain'
|
||||
|
||||
return HTTPResponse(status=200,
|
||||
headers=headers,
|
||||
content_type=mime_type,
|
||||
body_bytes=out_stream)
|
||||
|
||||
|
||||
async def file_stream(location, chunk_size=4096, mime_type=None, headers=None,
|
||||
_range=None):
|
||||
async def file_stream(
|
||||
location, chunk_size=4096, mime_type=None, headers=None,
|
||||
filename=None, _range=None):
|
||||
"""Return a streaming response object with file data.
|
||||
|
||||
:param location: Location of file on system.
|
||||
:param chunk_size: The size of each chunk in the stream (in bytes)
|
||||
:param mime_type: Specific mime_type.
|
||||
:param headers: Custom Headers.
|
||||
:param filename: Override filename.
|
||||
:param _range:
|
||||
"""
|
||||
filename = path.split(location)[-1]
|
||||
headers = headers or {}
|
||||
if filename:
|
||||
headers.setdefault(
|
||||
'Content-Disposition',
|
||||
'attachment; filename="{}"'.format(filename))
|
||||
filename = filename or path.split(location)[-1]
|
||||
|
||||
_file = await open_async(location, mode='rb')
|
||||
|
||||
|
|
114
sanic/router.py
114
sanic/router.py
|
@ -3,7 +3,7 @@ from collections import defaultdict, namedtuple
|
|||
from collections.abc import Iterable
|
||||
from functools import lru_cache
|
||||
|
||||
from sanic.exceptions import NotFound, InvalidUsage
|
||||
from sanic.exceptions import NotFound, MethodNotSupported
|
||||
from sanic.views import CompositionView
|
||||
|
||||
Route = namedtuple(
|
||||
|
@ -67,6 +67,8 @@ class Router:
|
|||
|
||||
def __init__(self):
|
||||
self.routes_all = {}
|
||||
self.routes_names = {}
|
||||
self.routes_static_files = {}
|
||||
self.routes_static = {}
|
||||
self.routes_dynamic = defaultdict(list)
|
||||
self.routes_always_check = []
|
||||
|
@ -91,6 +93,10 @@ class Router:
|
|||
pattern = 'string'
|
||||
if ':' in parameter_string:
|
||||
name, pattern = parameter_string.split(':', 1)
|
||||
if not name:
|
||||
raise ValueError(
|
||||
"Invalid parameter syntax: {}".format(parameter_string)
|
||||
)
|
||||
|
||||
default = (str, pattern)
|
||||
# Pull from pre-configured types
|
||||
|
@ -99,7 +105,7 @@ class Router:
|
|||
return name, _type, pattern
|
||||
|
||||
def add(self, uri, methods, handler, host=None, strict_slashes=False,
|
||||
version=None):
|
||||
version=None, name=None):
|
||||
"""Add a handler to the route list
|
||||
|
||||
:param uri: path to match
|
||||
|
@ -113,34 +119,38 @@ class Router:
|
|||
:return: Nothing
|
||||
"""
|
||||
if version is not None:
|
||||
if uri.startswith('/'):
|
||||
uri = "/".join(["/v{}".format(str(version)), uri[1:]])
|
||||
else:
|
||||
uri = "/".join(["/v{}".format(str(version)), uri])
|
||||
version = re.escape(str(version).strip('/').lstrip('v'))
|
||||
uri = "/".join(["/v{}".format(version), uri.lstrip('/')])
|
||||
# add regular version
|
||||
self._add(uri, methods, handler, host)
|
||||
self._add(uri, methods, handler, host, name)
|
||||
|
||||
if strict_slashes:
|
||||
return
|
||||
|
||||
# Add versions with and without trailing /
|
||||
slashed_methods = self.routes_all.get(uri + '/', frozenset({}))
|
||||
if isinstance(methods, Iterable):
|
||||
_slash_is_missing = all(method in slashed_methods for
|
||||
method in methods)
|
||||
else:
|
||||
_slash_is_missing = methods in slashed_methods
|
||||
|
||||
slash_is_missing = (
|
||||
not uri[-1] == '/'
|
||||
and not self.routes_all.get(uri + '/', False)
|
||||
not uri[-1] == '/' and not _slash_is_missing
|
||||
)
|
||||
without_slash_is_missing = (
|
||||
uri[-1] == '/'
|
||||
and not self.routes_all.get(uri[:-1], False)
|
||||
and not uri == '/'
|
||||
uri[-1] == '/' and not
|
||||
self.routes_all.get(uri[:-1], False) and not
|
||||
uri == '/'
|
||||
)
|
||||
# add version with trailing slash
|
||||
if slash_is_missing:
|
||||
self._add(uri + '/', methods, handler, host)
|
||||
self._add(uri + '/', methods, handler, host, name)
|
||||
# add version without trailing slash
|
||||
elif without_slash_is_missing:
|
||||
self._add(uri[:-1], methods, handler, host)
|
||||
self._add(uri[:-1], methods, handler, host, name)
|
||||
|
||||
def _add(self, uri, methods, handler, host=None):
|
||||
def _add(self, uri, methods, handler, host=None, name=None):
|
||||
"""Add a handler to the route list
|
||||
|
||||
:param uri: path to match
|
||||
|
@ -148,6 +158,7 @@ class Router:
|
|||
provided, any method is allowed
|
||||
:param handler: request handler function.
|
||||
When executed, it should provide a response object.
|
||||
:param name: user defined route name for url_for
|
||||
:return: Nothing
|
||||
"""
|
||||
if host is not None:
|
||||
|
@ -161,7 +172,7 @@ class Router:
|
|||
"host strings, not {!r}".format(host))
|
||||
|
||||
for host_ in host:
|
||||
self.add(uri, methods, handler, host_)
|
||||
self.add(uri, methods, handler, host_, name)
|
||||
return
|
||||
|
||||
# Dict for faster lookups of if method allowed
|
||||
|
@ -229,22 +240,38 @@ class Router:
|
|||
else:
|
||||
route = self.routes_all.get(uri)
|
||||
|
||||
# prefix the handler name with the blueprint name
|
||||
# if available
|
||||
# special prefix for static files
|
||||
is_static = False
|
||||
if name and name.startswith('_static_'):
|
||||
is_static = True
|
||||
name = name.split('_static_', 1)[-1]
|
||||
|
||||
if hasattr(handler, '__blueprintname__'):
|
||||
handler_name = '{}.{}'.format(
|
||||
handler.__blueprintname__, name or handler.__name__)
|
||||
else:
|
||||
handler_name = name or getattr(handler, '__name__', None)
|
||||
|
||||
if route:
|
||||
route = merge_route(route, methods, handler)
|
||||
else:
|
||||
# prefix the handler name with the blueprint name
|
||||
# if available
|
||||
if hasattr(handler, '__blueprintname__'):
|
||||
handler_name = '{}.{}'.format(
|
||||
handler.__blueprintname__, handler.__name__)
|
||||
else:
|
||||
handler_name = getattr(handler, '__name__', None)
|
||||
|
||||
route = Route(
|
||||
handler=handler, methods=methods, pattern=pattern,
|
||||
parameters=parameters, name=handler_name, uri=uri)
|
||||
|
||||
self.routes_all[uri] = route
|
||||
if is_static:
|
||||
pair = self.routes_static_files.get(handler_name)
|
||||
if not (pair and (pair[0] + '/' == uri or uri + '/' == pair[0])):
|
||||
self.routes_static_files[handler_name] = (uri, route)
|
||||
|
||||
else:
|
||||
pair = self.routes_names.get(handler_name)
|
||||
if not (pair and (pair[0] + '/' == uri or uri + '/' == pair[0])):
|
||||
self.routes_names[handler_name] = (uri, route)
|
||||
|
||||
if properties['unhashable']:
|
||||
self.routes_always_check.append(route)
|
||||
elif parameters:
|
||||
|
@ -265,6 +292,16 @@ class Router:
|
|||
uri = host + uri
|
||||
try:
|
||||
route = self.routes_all.pop(uri)
|
||||
for handler_name, pairs in self.routes_names.items():
|
||||
if pairs[0] == uri:
|
||||
self.routes_names.pop(handler_name)
|
||||
break
|
||||
|
||||
for handler_name, pairs in self.routes_static_files.items():
|
||||
if pairs[0] == uri:
|
||||
self.routes_static_files.pop(handler_name)
|
||||
break
|
||||
|
||||
except KeyError:
|
||||
raise RouteDoesNotExist("Route was not registered: {}".format(uri))
|
||||
|
||||
|
@ -280,20 +317,20 @@ class Router:
|
|||
self._get.cache_clear()
|
||||
|
||||
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
||||
def find_route_by_view_name(self, view_name):
|
||||
def find_route_by_view_name(self, view_name, name=None):
|
||||
"""Find a route in the router based on the specified view name.
|
||||
|
||||
:param view_name: string of view name to search by
|
||||
:param kwargs: additional params, usually for static files
|
||||
:return: tuple containing (uri, Route)
|
||||
"""
|
||||
if not view_name:
|
||||
return (None, None)
|
||||
|
||||
for uri, route in self.routes_all.items():
|
||||
if route.name == view_name:
|
||||
return uri, route
|
||||
if view_name == 'static' or view_name.endswith('.static'):
|
||||
return self.routes_static_files.get(name, (None, None))
|
||||
|
||||
return (None, None)
|
||||
return self.routes_names.get(view_name, (None, None))
|
||||
|
||||
def get(self, request):
|
||||
"""Get a request handler based on the URL of the request, or raises an
|
||||
|
@ -313,6 +350,16 @@ class Router:
|
|||
except NotFound:
|
||||
return self._get(request.path, request.method, '')
|
||||
|
||||
def get_supported_methods(self, url):
|
||||
"""Get a list of supported methods for a url and optional host.
|
||||
|
||||
:param url: URL string (including host)
|
||||
:return: frozenset of supported methods
|
||||
"""
|
||||
route = self.routes_all.get(url)
|
||||
# if methods are None then this logic will prevent an error
|
||||
return getattr(route, 'methods', None) or frozenset()
|
||||
|
||||
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
||||
def _get(self, url, method, host):
|
||||
"""Get a request handler based on the URL of the request, or raises an
|
||||
|
@ -325,9 +372,10 @@ class Router:
|
|||
url = host + url
|
||||
# Check against known static routes
|
||||
route = self.routes_static.get(url)
|
||||
method_not_supported = InvalidUsage(
|
||||
'Method {} not allowed for URL {}'.format(
|
||||
method, url), status_code=405)
|
||||
method_not_supported = MethodNotSupported(
|
||||
'Method {} not allowed for URL {}'.format(method, url),
|
||||
method=method,
|
||||
allowed_methods=self.get_supported_methods(url))
|
||||
if route:
|
||||
if route.methods and method not in route.methods:
|
||||
raise method_not_supported
|
||||
|
@ -370,7 +418,7 @@ class Router:
|
|||
"""
|
||||
try:
|
||||
handler = self.get(request)[0]
|
||||
except (NotFound, InvalidUsage):
|
||||
except (NotFound, MethodNotSupported):
|
||||
return False
|
||||
if (hasattr(handler, 'view_class') and
|
||||
hasattr(handler.view_class, request.method.lower())):
|
||||
|
|
295
sanic/server.py
295
sanic/server.py
|
@ -24,11 +24,12 @@ try:
|
|||
except ImportError:
|
||||
async_loop = asyncio
|
||||
|
||||
from sanic.log import log, netlog
|
||||
from sanic.log import logger, access_logger
|
||||
from sanic.response import HTTPResponse
|
||||
from sanic.request import Request
|
||||
from sanic.exceptions import (
|
||||
RequestTimeout, PayloadTooLarge, InvalidUsage, ServerError)
|
||||
RequestTimeout, PayloadTooLarge, InvalidUsage, ServerError,
|
||||
ServiceUnavailable)
|
||||
|
||||
current_time = None
|
||||
|
||||
|
@ -63,17 +64,20 @@ class HttpProtocol(asyncio.Protocol):
|
|||
# request params
|
||||
'parser', 'request', 'url', 'headers',
|
||||
# request config
|
||||
'request_handler', 'request_timeout', 'request_max_size',
|
||||
'request_class', 'is_request_stream', 'router',
|
||||
# enable or disable access log / error log purpose
|
||||
'has_log',
|
||||
'request_handler', 'request_timeout', 'response_timeout',
|
||||
'keep_alive_timeout', 'request_max_size', 'request_class',
|
||||
'is_request_stream', 'router',
|
||||
# enable or disable access log purpose
|
||||
'access_log',
|
||||
# connection management
|
||||
'_total_request_size', '_timeout_handler', '_last_communication_time',
|
||||
'_is_stream_handler')
|
||||
'_total_request_size', '_request_timeout_handler',
|
||||
'_response_timeout_handler', '_keep_alive_timeout_handler',
|
||||
'_last_request_time', '_last_response_time', '_is_stream_handler')
|
||||
|
||||
def __init__(self, *, loop, request_handler, error_handler,
|
||||
signal=Signal(), connections=set(), request_timeout=60,
|
||||
request_max_size=None, request_class=None, has_log=True,
|
||||
response_timeout=60, keep_alive_timeout=5,
|
||||
request_max_size=None, request_class=None, access_log=True,
|
||||
keep_alive=True, is_request_stream=False, router=None,
|
||||
state=None, debug=False, **kwargs):
|
||||
self.loop = loop
|
||||
|
@ -84,18 +88,23 @@ class HttpProtocol(asyncio.Protocol):
|
|||
self.headers = None
|
||||
self.router = router
|
||||
self.signal = signal
|
||||
self.has_log = has_log
|
||||
self.access_log = access_log
|
||||
self.connections = connections
|
||||
self.request_handler = request_handler
|
||||
self.error_handler = error_handler
|
||||
self.request_timeout = request_timeout
|
||||
self.response_timeout = response_timeout
|
||||
self.keep_alive_timeout = keep_alive_timeout
|
||||
self.request_max_size = request_max_size
|
||||
self.request_class = request_class or Request
|
||||
self.is_request_stream = is_request_stream
|
||||
self._is_stream_handler = False
|
||||
self._total_request_size = 0
|
||||
self._timeout_handler = None
|
||||
self._request_timeout_handler = None
|
||||
self._response_timeout_handler = None
|
||||
self._keep_alive_timeout_handler = None
|
||||
self._last_request_time = None
|
||||
self._last_response_time = None
|
||||
self._request_handler_task = None
|
||||
self._request_stream_task = None
|
||||
self._keep_alive = keep_alive
|
||||
|
@ -118,22 +127,32 @@ class HttpProtocol(asyncio.Protocol):
|
|||
|
||||
def connection_made(self, transport):
|
||||
self.connections.add(self)
|
||||
self._timeout_handler = self.loop.call_later(
|
||||
self.request_timeout, self.connection_timeout)
|
||||
self._request_timeout_handler = self.loop.call_later(
|
||||
self.request_timeout, self.request_timeout_callback)
|
||||
self.transport = transport
|
||||
self._last_request_time = current_time
|
||||
|
||||
def connection_lost(self, exc):
|
||||
self.connections.discard(self)
|
||||
self._timeout_handler.cancel()
|
||||
if self._request_timeout_handler:
|
||||
self._request_timeout_handler.cancel()
|
||||
if self._response_timeout_handler:
|
||||
self._response_timeout_handler.cancel()
|
||||
if self._keep_alive_timeout_handler:
|
||||
self._keep_alive_timeout_handler.cancel()
|
||||
|
||||
def connection_timeout(self):
|
||||
# Check if
|
||||
def request_timeout_callback(self):
|
||||
# See the docstring in the RequestTimeout exception, to see
|
||||
# exactly what this timeout is checking for.
|
||||
# Check if elapsed time since request initiated exceeds our
|
||||
# configured maximum request timeout value
|
||||
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))
|
||||
self._request_timeout_handler = (
|
||||
self.loop.call_later(time_left,
|
||||
self.request_timeout_callback)
|
||||
)
|
||||
else:
|
||||
if self._request_stream_task:
|
||||
self._request_stream_task.cancel()
|
||||
|
@ -144,6 +163,41 @@ class HttpProtocol(asyncio.Protocol):
|
|||
except RequestTimeout as exception:
|
||||
self.write_error(exception)
|
||||
|
||||
def response_timeout_callback(self):
|
||||
# Check if elapsed time since response was initiated exceeds our
|
||||
# configured maximum request timeout value
|
||||
time_elapsed = current_time - self._last_request_time
|
||||
if time_elapsed < self.response_timeout:
|
||||
time_left = self.response_timeout - time_elapsed
|
||||
self._response_timeout_handler = (
|
||||
self.loop.call_later(time_left,
|
||||
self.response_timeout_callback)
|
||||
)
|
||||
else:
|
||||
if self._request_stream_task:
|
||||
self._request_stream_task.cancel()
|
||||
if self._request_handler_task:
|
||||
self._request_handler_task.cancel()
|
||||
try:
|
||||
raise ServiceUnavailable('Response Timeout')
|
||||
except ServiceUnavailable as exception:
|
||||
self.write_error(exception)
|
||||
|
||||
def keep_alive_timeout_callback(self):
|
||||
# Check if elapsed time since last response exceeds our configured
|
||||
# maximum keep alive timeout value
|
||||
time_elapsed = current_time - self._last_response_time
|
||||
if time_elapsed < self.keep_alive_timeout:
|
||||
time_left = self.keep_alive_timeout - time_elapsed
|
||||
self._keep_alive_timeout_handler = (
|
||||
self.loop.call_later(time_left,
|
||||
self.keep_alive_timeout_callback)
|
||||
)
|
||||
else:
|
||||
logger.info('KeepAlive Timeout. Closing connection.')
|
||||
self.transport.close()
|
||||
self.transport = None
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Parsing
|
||||
# -------------------------------------------- #
|
||||
|
@ -189,10 +243,12 @@ class HttpProtocol(asyncio.Protocol):
|
|||
and int(value) > self.request_max_size:
|
||||
exception = PayloadTooLarge('Payload Too Large')
|
||||
self.write_error(exception)
|
||||
|
||||
try:
|
||||
value = value.decode()
|
||||
except UnicodeDecodeError:
|
||||
value = value.decode('latin_1')
|
||||
self.headers.append(
|
||||
(self._header_fragment.decode().casefold(),
|
||||
value.decode()))
|
||||
(self._header_fragment.decode().casefold(), value))
|
||||
|
||||
self._header_fragment = b''
|
||||
|
||||
|
@ -204,6 +260,11 @@ class HttpProtocol(asyncio.Protocol):
|
|||
method=self.parser.get_method().decode(),
|
||||
transport=self.transport
|
||||
)
|
||||
# Remove any existing KeepAlive handler here,
|
||||
# It will be recreated if required on the new request.
|
||||
if self._keep_alive_timeout_handler:
|
||||
self._keep_alive_timeout_handler.cancel()
|
||||
self._keep_alive_timeout_handler = None
|
||||
if self.is_request_stream:
|
||||
self._is_stream_handler = self.router.is_stream_handler(
|
||||
self.request)
|
||||
|
@ -219,6 +280,11 @@ class HttpProtocol(asyncio.Protocol):
|
|||
self.request.body.append(body)
|
||||
|
||||
def on_message_complete(self):
|
||||
# Entire request (headers and whole body) is received.
|
||||
# We can cancel and remove the request timeout handler now.
|
||||
if self._request_timeout_handler:
|
||||
self._request_timeout_handler.cancel()
|
||||
self._request_timeout_handler = None
|
||||
if self.is_request_stream and self._is_stream_handler:
|
||||
self._request_stream_task = self.loop.create_task(
|
||||
self.request.stream.put(None))
|
||||
|
@ -227,6 +293,9 @@ class HttpProtocol(asyncio.Protocol):
|
|||
self.execute_request_handler()
|
||||
|
||||
def execute_request_handler(self):
|
||||
self._response_timeout_handler = self.loop.call_later(
|
||||
self.response_timeout, self.response_timeout_callback)
|
||||
self._last_request_time = current_time
|
||||
self._request_handler_task = self.loop.create_task(
|
||||
self.request_handler(
|
||||
self.request,
|
||||
|
@ -236,35 +305,53 @@ class HttpProtocol(asyncio.Protocol):
|
|||
# -------------------------------------------- #
|
||||
# Responding
|
||||
# -------------------------------------------- #
|
||||
def log_response(self, response):
|
||||
if self.access_log:
|
||||
extra = {
|
||||
'status': getattr(response, 'status', 0),
|
||||
}
|
||||
|
||||
if isinstance(response, HTTPResponse):
|
||||
extra['byte'] = len(response.body)
|
||||
else:
|
||||
extra['byte'] = -1
|
||||
|
||||
extra['host'] = 'UNKNOWN'
|
||||
if self.request is not None:
|
||||
if self.request.ip:
|
||||
extra['host'] = '{0[0]}:{0[1]}'.format(self.request.ip)
|
||||
|
||||
extra['request'] = '{0} {1}'.format(self.request.method,
|
||||
self.request.url)
|
||||
else:
|
||||
extra['request'] = 'nil'
|
||||
|
||||
access_logger.info('', extra=extra)
|
||||
|
||||
def write_response(self, response):
|
||||
"""
|
||||
Writes response content synchronously to the transport.
|
||||
"""
|
||||
if self._response_timeout_handler:
|
||||
self._response_timeout_handler.cancel()
|
||||
self._response_timeout_handler = None
|
||||
try:
|
||||
keep_alive = self.keep_alive
|
||||
self.transport.write(
|
||||
response.output(
|
||||
self.request.version, keep_alive,
|
||||
self.request_timeout))
|
||||
if self.has_log:
|
||||
netlog.info('', extra={
|
||||
'status': response.status,
|
||||
'byte': len(response.body),
|
||||
'host': '{0}:{1}'.format(self.request.ip[0],
|
||||
self.request.ip[1]),
|
||||
'request': '{0} {1}'.format(self.request.method,
|
||||
self.request.url)
|
||||
})
|
||||
self.keep_alive_timeout))
|
||||
self.log_response(response)
|
||||
except AttributeError:
|
||||
log.error(
|
||||
('Invalid response object for url {}, '
|
||||
'Expected Type: HTTPResponse, Actual Type: {}').format(
|
||||
self.url, type(response)))
|
||||
logger.error('Invalid response object for url %s, '
|
||||
'Expected Type: HTTPResponse, Actual Type: %s',
|
||||
self.url, type(response))
|
||||
self.write_error(ServerError('Invalid response type'))
|
||||
except RuntimeError:
|
||||
log.error(
|
||||
'Connection lost before response written @ {}'.format(
|
||||
self.request.ip))
|
||||
if self._debug:
|
||||
logger.error('Connection lost before response written @ %s',
|
||||
self.request.ip)
|
||||
keep_alive = False
|
||||
except Exception as e:
|
||||
self.bail_out(
|
||||
"Writing response failed, connection closed {}".format(
|
||||
|
@ -272,8 +359,12 @@ class HttpProtocol(asyncio.Protocol):
|
|||
finally:
|
||||
if not keep_alive:
|
||||
self.transport.close()
|
||||
self.transport = None
|
||||
else:
|
||||
self._last_request_time = current_time
|
||||
self._keep_alive_timeout_handler = self.loop.call_later(
|
||||
self.keep_alive_timeout,
|
||||
self.keep_alive_timeout_callback)
|
||||
self._last_response_time = current_time
|
||||
self.cleanup()
|
||||
|
||||
async def stream_response(self, response):
|
||||
|
@ -282,31 +373,25 @@ class HttpProtocol(asyncio.Protocol):
|
|||
the transport to the response so the response consumer can
|
||||
write to the response as needed.
|
||||
"""
|
||||
|
||||
if self._response_timeout_handler:
|
||||
self._response_timeout_handler.cancel()
|
||||
self._response_timeout_handler = None
|
||||
try:
|
||||
keep_alive = self.keep_alive
|
||||
response.transport = self.transport
|
||||
await response.stream(
|
||||
self.request.version, keep_alive, self.request_timeout)
|
||||
if self.has_log:
|
||||
netlog.info('', extra={
|
||||
'status': response.status,
|
||||
'byte': -1,
|
||||
'host': '{0}:{1}'.format(self.request.ip[0],
|
||||
self.request.ip[1]),
|
||||
'request': '{0} {1}'.format(self.request.method,
|
||||
self.request.url)
|
||||
})
|
||||
self.request.version, keep_alive, self.keep_alive_timeout)
|
||||
self.log_response(response)
|
||||
except AttributeError:
|
||||
log.error(
|
||||
('Invalid response object for url {}, '
|
||||
'Expected Type: HTTPResponse, Actual Type: {}').format(
|
||||
self.url, type(response)))
|
||||
logger.error('Invalid response object for url %s, '
|
||||
'Expected Type: HTTPResponse, Actual Type: %s',
|
||||
self.url, type(response))
|
||||
self.write_error(ServerError('Invalid response type'))
|
||||
except RuntimeError:
|
||||
log.error(
|
||||
'Connection lost before response written @ {}'.format(
|
||||
self.request.ip))
|
||||
if self._debug:
|
||||
logger.error('Connection lost before response written @ %s',
|
||||
self.request.ip)
|
||||
keep_alive = False
|
||||
except Exception as e:
|
||||
self.bail_out(
|
||||
"Writing response failed, connection closed {}".format(
|
||||
|
@ -314,59 +399,55 @@ class HttpProtocol(asyncio.Protocol):
|
|||
finally:
|
||||
if not keep_alive:
|
||||
self.transport.close()
|
||||
self.transport = None
|
||||
else:
|
||||
self._last_request_time = current_time
|
||||
self._keep_alive_timeout_handler = self.loop.call_later(
|
||||
self.keep_alive_timeout,
|
||||
self.keep_alive_timeout_callback)
|
||||
self._last_response_time = current_time
|
||||
self.cleanup()
|
||||
|
||||
def write_error(self, exception):
|
||||
# An error _is_ a response.
|
||||
# Don't throw a response timeout, when a response _is_ given.
|
||||
if self._response_timeout_handler:
|
||||
self._response_timeout_handler.cancel()
|
||||
self._response_timeout_handler = None
|
||||
response = None
|
||||
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))
|
||||
except RuntimeError:
|
||||
log.error(
|
||||
'Connection lost before error written @ {}'.format(
|
||||
self.request.ip if self.request else 'Unknown'))
|
||||
if self._debug:
|
||||
logger.error('Connection lost before error written @ %s',
|
||||
self.request.ip if self.request else 'Unknown')
|
||||
except Exception as e:
|
||||
self.bail_out(
|
||||
"Writing error failed, connection closed {}".format(repr(e)),
|
||||
from_error=True)
|
||||
"Writing error failed, connection closed {}".format(
|
||||
repr(e)), from_error=True
|
||||
)
|
||||
finally:
|
||||
if self.has_log:
|
||||
extra = dict()
|
||||
if isinstance(response, HTTPResponse):
|
||||
extra['status'] = response.status
|
||||
extra['byte'] = len(response.body)
|
||||
else:
|
||||
extra['status'] = 0
|
||||
extra['byte'] = -1
|
||||
if self.request:
|
||||
extra['host'] = '%s:%d' % self.request.ip,
|
||||
extra['request'] = '%s %s' % (self.request.method,
|
||||
self.url)
|
||||
else:
|
||||
extra['host'] = 'UNKNOWN'
|
||||
extra['request'] = 'nil'
|
||||
if self.parser and not (self.keep_alive
|
||||
and extra['status'] == 408):
|
||||
netlog.info('', extra=extra)
|
||||
if self.parser and (self.keep_alive
|
||||
or getattr(response, 'status', 0) == 408):
|
||||
self.log_response(response)
|
||||
self.transport.close()
|
||||
|
||||
def bail_out(self, message, from_error=False):
|
||||
if from_error or self.transport.is_closing():
|
||||
log.error(
|
||||
("Transport closed @ {} and exception "
|
||||
"experienced during error handling").format(
|
||||
self.transport.get_extra_info('peername')))
|
||||
log.debug(
|
||||
'Exception:\n{}'.format(traceback.format_exc()))
|
||||
logger.error("Transport closed @ %s and exception "
|
||||
"experienced during error handling",
|
||||
self.transport.get_extra_info('peername'))
|
||||
logger.debug('Exception:\n%s', traceback.format_exc())
|
||||
else:
|
||||
exception = ServerError(message)
|
||||
self.write_error(exception)
|
||||
log.error(message)
|
||||
logger.error(message)
|
||||
|
||||
def cleanup(self):
|
||||
"""This is called when KeepAlive feature is used,
|
||||
it resets the connection in order for it to be able
|
||||
to handle receiving another request on the same connection."""
|
||||
self.parser = None
|
||||
self.request = None
|
||||
self.url = None
|
||||
|
@ -421,12 +502,13 @@ def trigger_events(events, loop):
|
|||
|
||||
def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
after_start=None, before_stop=None, after_stop=None, debug=False,
|
||||
request_timeout=60, ssl=None, sock=None, request_max_size=None,
|
||||
reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100,
|
||||
request_timeout=60, response_timeout=60, keep_alive_timeout=5,
|
||||
ssl=None, sock=None, request_max_size=None, reuse_port=False,
|
||||
loop=None, protocol=HttpProtocol, backlog=100,
|
||||
register_sys_signals=True, run_async=False, connections=None,
|
||||
signal=Signal(), request_class=None, has_log=True, keep_alive=True,
|
||||
is_request_stream=False, router=None, websocket_max_size=None,
|
||||
websocket_max_queue=None, state=None,
|
||||
signal=Signal(), request_class=None, access_log=True,
|
||||
keep_alive=True, is_request_stream=False, router=None,
|
||||
websocket_max_size=None, websocket_max_queue=None, state=None,
|
||||
graceful_shutdown_timeout=15.0):
|
||||
"""Start asynchronous HTTP Server on an individual process.
|
||||
|
||||
|
@ -446,6 +528,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
|||
`app` instance and `loop`
|
||||
:param debug: enables debug output (slows server)
|
||||
:param request_timeout: time in seconds
|
||||
:param response_timeout: time in seconds
|
||||
:param keep_alive_timeout: time in seconds
|
||||
:param ssl: SSLContext
|
||||
:param sock: Socket for the server to accept connections from
|
||||
:param request_max_size: size in bytes, `None` for no limit
|
||||
|
@ -453,7 +537,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
|||
:param loop: asyncio compatible event loop
|
||||
:param protocol: subclass of asyncio protocol class
|
||||
:param request_class: Request class to use
|
||||
:param has_log: disable/enable access log and error log
|
||||
:param access_log: disable/enable access log
|
||||
:param is_request_stream: disable/enable Request.stream
|
||||
:param router: Router object
|
||||
:return: Nothing
|
||||
|
@ -474,9 +558,11 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
|||
request_handler=request_handler,
|
||||
error_handler=error_handler,
|
||||
request_timeout=request_timeout,
|
||||
response_timeout=response_timeout,
|
||||
keep_alive_timeout=keep_alive_timeout,
|
||||
request_max_size=request_max_size,
|
||||
request_class=request_class,
|
||||
has_log=has_log,
|
||||
access_log=access_log,
|
||||
keep_alive=keep_alive,
|
||||
is_request_stream=is_request_stream,
|
||||
router=router,
|
||||
|
@ -507,8 +593,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
|||
|
||||
try:
|
||||
http_server = loop.run_until_complete(server_coroutine)
|
||||
except:
|
||||
log.exception("Unable to start server")
|
||||
except BaseException:
|
||||
logger.exception("Unable to start server")
|
||||
return
|
||||
|
||||
trigger_events(after_start, loop)
|
||||
|
@ -519,14 +605,14 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
|||
try:
|
||||
loop.add_signal_handler(_signal, loop.stop)
|
||||
except NotImplementedError:
|
||||
log.warn('Sanic tried to use loop.add_signal_handler but it is'
|
||||
' not implemented on this platform.')
|
||||
logger.warning('Sanic tried to use loop.add_signal_handler '
|
||||
'but it is not implemented on this platform.')
|
||||
pid = os.getpid()
|
||||
try:
|
||||
log.info('Starting worker [{}]'.format(pid))
|
||||
logger.info('Starting worker [%s]', pid)
|
||||
loop.run_forever()
|
||||
finally:
|
||||
log.info("Stopping worker [{}]".format(pid))
|
||||
logger.info("Stopping worker [%s]", pid)
|
||||
|
||||
# Run the on_stop function if provided
|
||||
trigger_events(before_stop, loop)
|
||||
|
@ -554,7 +640,9 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
|||
coros = []
|
||||
for conn in connections:
|
||||
if hasattr(conn, "websocket") and conn.websocket:
|
||||
coros.append(conn.websocket.close_connection(force=True))
|
||||
coros.append(
|
||||
conn.websocket.close_connection(after_handshake=True)
|
||||
)
|
||||
else:
|
||||
conn.close()
|
||||
|
||||
|
@ -588,8 +676,7 @@ def serve_multiple(server_settings, workers):
|
|||
server_settings['port'] = None
|
||||
|
||||
def sig_handler(signal, frame):
|
||||
log.info("Received signal {}. Shutting down.".format(
|
||||
Signals(signal).name))
|
||||
logger.info("Received signal %s. Shutting down.", Signals(signal).name)
|
||||
for process in processes:
|
||||
os.kill(process.pid, SIGINT)
|
||||
|
||||
|
|
|
@ -18,7 +18,8 @@ from sanic.response import file, file_stream, HTTPResponse
|
|||
|
||||
def register(app, uri, file_or_directory, pattern,
|
||||
use_modified_since, use_content_range,
|
||||
stream_large_files):
|
||||
stream_large_files, name='static', host=None,
|
||||
strict_slashes=None):
|
||||
# TODO: Though sanic is not a file server, I feel like we should at least
|
||||
# make a good effort here. Modified-since is nice, but we could
|
||||
# also look into etags, expires, and caching
|
||||
|
@ -39,6 +40,7 @@ def register(app, uri, file_or_directory, pattern,
|
|||
than the file() handler to send the file
|
||||
If this is an integer, this represents the
|
||||
threshold size to switch to file_stream()
|
||||
:param name: user defined name used for url_for
|
||||
"""
|
||||
# If we're not trying to match a file directly,
|
||||
# serve from the folder
|
||||
|
@ -102,7 +104,7 @@ def register(app, uri, file_or_directory, pattern,
|
|||
if isinstance(stream_large_files, int):
|
||||
threshold = stream_large_files
|
||||
else:
|
||||
threshold = 1024*1000
|
||||
threshold = 1024 * 1024
|
||||
|
||||
if not stats:
|
||||
stats = await stat(file_path)
|
||||
|
@ -117,4 +119,9 @@ def register(app, uri, file_or_directory, pattern,
|
|||
path=file_or_directory,
|
||||
relative_url=file_uri)
|
||||
|
||||
app.route(uri, methods=['GET', 'HEAD'])(_handler)
|
||||
# special prefix for static files
|
||||
if not name.startswith('_static_'):
|
||||
name = '_static_{}'.format(name)
|
||||
|
||||
app.route(uri, methods=['GET', 'HEAD'], name=name, host=host,
|
||||
strict_slashes=strict_slashes)(_handler)
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import traceback
|
||||
from json import JSONDecodeError
|
||||
|
||||
from sanic.log import log
|
||||
from sanic.log import logger
|
||||
|
||||
HOST = '127.0.0.1'
|
||||
PORT = 42101
|
||||
|
||||
|
||||
class SanicTestClient:
|
||||
def __init__(self, app):
|
||||
def __init__(self, app, port=PORT):
|
||||
self.app = app
|
||||
self.port = port
|
||||
|
||||
async def _local_request(self, method, uri, cookies=None, *args, **kwargs):
|
||||
import aiohttp
|
||||
|
@ -17,9 +18,9 @@ class SanicTestClient:
|
|||
url = uri
|
||||
else:
|
||||
url = 'http://{host}:{port}{uri}'.format(
|
||||
host=HOST, port=PORT, uri=uri)
|
||||
host=HOST, port=self.port, uri=uri)
|
||||
|
||||
log.info(url)
|
||||
logger.info(url)
|
||||
conn = aiohttp.TCPConnector(verify_ssl=False)
|
||||
async with aiohttp.ClientSession(
|
||||
cookies=cookies, connector=conn) as session:
|
||||
|
@ -61,12 +62,12 @@ class SanicTestClient:
|
|||
**request_kwargs)
|
||||
results[-1] = response
|
||||
except Exception as e:
|
||||
log.error(
|
||||
logger.error(
|
||||
'Exception:\n{}'.format(traceback.format_exc()))
|
||||
exceptions.append(e)
|
||||
self.app.stop()
|
||||
|
||||
self.app.run(host=HOST, debug=debug, port=PORT, **server_kwargs)
|
||||
self.app.run(host=HOST, debug=debug, port=self.port, **server_kwargs)
|
||||
self.app.listeners['after_server_start'].pop()
|
||||
|
||||
if exceptions:
|
||||
|
@ -76,14 +77,14 @@ class SanicTestClient:
|
|||
try:
|
||||
request, response = results
|
||||
return request, response
|
||||
except:
|
||||
except BaseException:
|
||||
raise ValueError(
|
||||
"Request and response object expected, got ({})".format(
|
||||
results))
|
||||
else:
|
||||
try:
|
||||
return results[-1]
|
||||
except:
|
||||
except BaseException:
|
||||
raise ValueError(
|
||||
"Request object expected, got ({})".format(results))
|
||||
|
||||
|
|
|
@ -13,10 +13,18 @@ class WebSocketProtocol(HttpProtocol):
|
|||
self.websocket_max_size = websocket_max_size
|
||||
self.websocket_max_queue = websocket_max_queue
|
||||
|
||||
def connection_timeout(self):
|
||||
# timeouts make no sense for websocket routes
|
||||
# timeouts make no sense for websocket routes
|
||||
def request_timeout_callback(self):
|
||||
if self.websocket is None:
|
||||
super().connection_timeout()
|
||||
super().request_timeout_callback()
|
||||
|
||||
def response_timeout_callback(self):
|
||||
if self.websocket is None:
|
||||
super().response_timeout_callback()
|
||||
|
||||
def keep_alive_timeout_callback(self):
|
||||
if self.websocket is None:
|
||||
super().keep_alive_timeout_callback()
|
||||
|
||||
def connection_lost(self, exc):
|
||||
if self.websocket is not None:
|
||||
|
@ -41,7 +49,7 @@ class WebSocketProtocol(HttpProtocol):
|
|||
else:
|
||||
super().write_response(response)
|
||||
|
||||
async def websocket_handshake(self, request):
|
||||
async def websocket_handshake(self, request, subprotocols=None):
|
||||
# let the websockets package do the handshake with the client
|
||||
headers = []
|
||||
|
||||
|
@ -57,6 +65,17 @@ class WebSocketProtocol(HttpProtocol):
|
|||
except InvalidHandshake:
|
||||
raise InvalidUsage('Invalid websocket request')
|
||||
|
||||
subprotocol = None
|
||||
if subprotocols and 'Sec-Websocket-Protocol' in request.headers:
|
||||
# select a subprotocol
|
||||
client_subprotocols = [p.strip() for p in request.headers[
|
||||
'Sec-Websocket-Protocol'].split(',')]
|
||||
for p in client_subprotocols:
|
||||
if p in subprotocols:
|
||||
subprotocol = p
|
||||
set_header('Sec-Websocket-Protocol', subprotocol)
|
||||
break
|
||||
|
||||
# write the 101 response back to the client
|
||||
rv = b'HTTP/1.1 101 Switching Protocols\r\n'
|
||||
for k, v in headers:
|
||||
|
@ -69,5 +88,7 @@ class WebSocketProtocol(HttpProtocol):
|
|||
max_size=self.websocket_max_size,
|
||||
max_queue=self.websocket_max_queue
|
||||
)
|
||||
self.websocket.subprotocol = subprotocol
|
||||
self.websocket.connection_made(request.transport)
|
||||
self.websocket.connection_open()
|
||||
return self.websocket
|
||||
|
|
|
@ -23,6 +23,9 @@ from sanic.websocket import WebSocketProtocol
|
|||
|
||||
class GunicornWorker(base.Worker):
|
||||
|
||||
http_protocol = HttpProtocol
|
||||
websocket_protocol = WebSocketProtocol
|
||||
|
||||
def __init__(self, *args, **kw): # pragma: no cover
|
||||
super().__init__(*args, **kw)
|
||||
cfg = self.cfg
|
||||
|
@ -46,8 +49,9 @@ class GunicornWorker(base.Worker):
|
|||
|
||||
def run(self):
|
||||
is_debug = self.log.loglevel == logging.DEBUG
|
||||
protocol = (WebSocketProtocol if self.app.callable.websocket_enabled
|
||||
else HttpProtocol)
|
||||
protocol = (
|
||||
self.websocket_protocol if self.app.callable.websocket_enabled
|
||||
else self.http_protocol)
|
||||
self._server_settings = self.app.callable._helper(
|
||||
loop=self.loop,
|
||||
debug=is_debug,
|
||||
|
@ -70,13 +74,13 @@ class GunicornWorker(base.Worker):
|
|||
trigger_events(self._server_settings.get('before_stop', []),
|
||||
self.loop)
|
||||
self.loop.run_until_complete(self.close())
|
||||
except:
|
||||
except BaseException:
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
try:
|
||||
trigger_events(self._server_settings.get('after_stop', []),
|
||||
self.loop)
|
||||
except:
|
||||
except BaseException:
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
self.loop.close()
|
||||
|
@ -111,7 +115,9 @@ class GunicornWorker(base.Worker):
|
|||
coros = []
|
||||
for conn in self.connections:
|
||||
if hasattr(conn, "websocket") and conn.websocket:
|
||||
coros.append(conn.websocket.close_connection(force=True))
|
||||
coros.append(
|
||||
conn.websocket.close_connection(after_handshake=False)
|
||||
)
|
||||
else:
|
||||
conn.close()
|
||||
_shutdown = asyncio.gather(*coros, loop=self.loop)
|
||||
|
@ -142,9 +148,8 @@ class GunicornWorker(base.Worker):
|
|||
)
|
||||
if self.max_requests and req_count > self.max_requests:
|
||||
self.alive = False
|
||||
self.log.info(
|
||||
"Max requests exceeded, shutting down: %s", self
|
||||
)
|
||||
self.log.info("Max requests exceeded, shutting down: %s",
|
||||
self)
|
||||
elif pid == os.getpid() and self.ppid != os.getppid():
|
||||
self.alive = False
|
||||
self.log.info("Parent changed, shutting down: %s", self)
|
||||
|
|
2
setup.py
2
setup.py
|
@ -59,7 +59,7 @@ requirements = [
|
|||
uvloop,
|
||||
ujson,
|
||||
'aiofiles>=0.3.0',
|
||||
'websockets>=3.2',
|
||||
'websockets>=4.0',
|
||||
]
|
||||
if strtobool(os.environ.get("SANIC_NO_UJSON", "no")):
|
||||
print("Installing without uJSON")
|
||||
|
|
1
tests/static/bp/decode me.txt
Normal file
1
tests/static/bp/decode me.txt
Normal file
|
@ -0,0 +1 @@
|
|||
I am just a regular static file that needs to have its uri decoded
|
BIN
tests/static/bp/python.png
Normal file
BIN
tests/static/bp/python.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
1
tests/static/bp/test.file
Normal file
1
tests/static/bp/test.file
Normal file
|
@ -0,0 +1 @@
|
|||
I am just a regular static file
|
|
@ -78,6 +78,65 @@ def test_bp_strict_slash():
|
|||
request, response = app.test_client.post('/post')
|
||||
assert response.status == 404
|
||||
|
||||
def test_bp_strict_slash_default_value():
|
||||
app = Sanic('test_route_strict_slash')
|
||||
bp = Blueprint('test_text', strict_slashes=True)
|
||||
|
||||
@bp.get('/get')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
@bp.post('/post/')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
request, response = app.test_client.get('/get/')
|
||||
assert response.status == 404
|
||||
|
||||
request, response = app.test_client.post('/post')
|
||||
assert response.status == 404
|
||||
|
||||
def test_bp_strict_slash_without_passing_default_value():
|
||||
app = Sanic('test_route_strict_slash')
|
||||
bp = Blueprint('test_text')
|
||||
|
||||
@bp.get('/get')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
@bp.post('/post/')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
request, response = app.test_client.get('/get/')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = app.test_client.post('/post')
|
||||
assert response.text == 'OK'
|
||||
|
||||
def test_bp_strict_slash_default_value_can_be_overwritten():
|
||||
app = Sanic('test_route_strict_slash')
|
||||
bp = Blueprint('test_text', strict_slashes=True)
|
||||
|
||||
@bp.get('/get', strict_slashes=False)
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
@bp.post('/post/', strict_slashes=False)
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
request, response = app.test_client.get('/get/')
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = app.test_client.post('/post')
|
||||
assert response.text == 'OK'
|
||||
|
||||
def test_bp_with_url_prefix():
|
||||
app = Sanic('test_text')
|
||||
|
|
|
@ -19,15 +19,21 @@ def test_load_from_object():
|
|||
def test_auto_load_env():
|
||||
environ["SANIC_TEST_ANSWER"] = "42"
|
||||
app = Sanic()
|
||||
assert app.config.TEST_ANSWER == "42"
|
||||
assert app.config.TEST_ANSWER == 42
|
||||
del environ["SANIC_TEST_ANSWER"]
|
||||
|
||||
def test_auto_load_env():
|
||||
def test_dont_load_env():
|
||||
environ["SANIC_TEST_ANSWER"] = "42"
|
||||
app = Sanic(load_env=False)
|
||||
assert getattr(app.config, 'TEST_ANSWER', None) == None
|
||||
del environ["SANIC_TEST_ANSWER"]
|
||||
|
||||
def test_load_env_prefix():
|
||||
environ["MYAPP_TEST_ANSWER"] = "42"
|
||||
app = Sanic(load_env='MYAPP_')
|
||||
assert app.config.TEST_ANSWER == 42
|
||||
del environ["MYAPP_TEST_ANSWER"]
|
||||
|
||||
def test_load_from_file():
|
||||
app = Sanic('test_load_from_file')
|
||||
config = b"""
|
||||
|
|
|
@ -25,6 +25,25 @@ def test_cookies():
|
|||
assert response.text == 'Cookies are: working!'
|
||||
assert response_cookies['right_back'].value == 'at you'
|
||||
|
||||
@pytest.mark.parametrize("httponly,expected", [
|
||||
(False, False),
|
||||
(True, True),
|
||||
])
|
||||
def test_false_cookies_encoded(httponly, expected):
|
||||
app = Sanic('test_text')
|
||||
|
||||
@app.route('/')
|
||||
def handler(request):
|
||||
response = text('hello cookies')
|
||||
response.cookies['hello'] = 'world'
|
||||
response.cookies['hello']['httponly'] = httponly
|
||||
return text(response.cookies['hello'].encode('utf8'))
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
assert ('HttpOnly' in response.text) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("httponly,expected", [
|
||||
(False, False),
|
||||
(True, True),
|
||||
|
@ -34,7 +53,7 @@ def test_false_cookies(httponly, expected):
|
|||
|
||||
@app.route('/')
|
||||
def handler(request):
|
||||
response = text('Cookies are: {}'.format(request.cookies['test']))
|
||||
response = text('hello cookies')
|
||||
response.cookies['right_back'] = 'at you'
|
||||
response.cookies['right_back']['httponly'] = httponly
|
||||
return response
|
||||
|
@ -43,7 +62,7 @@ def test_false_cookies(httponly, expected):
|
|||
response_cookies = SimpleCookie()
|
||||
response_cookies.load(response.headers.get('Set-Cookie', {}))
|
||||
|
||||
'HttpOnly' in response_cookies == expected
|
||||
assert ('HttpOnly' in response_cookies['right_back'].output()) == expected
|
||||
|
||||
def test_http2_cookies():
|
||||
app = Sanic('test_http2_cookies')
|
||||
|
|
|
@ -31,31 +31,38 @@ def exception_app():
|
|||
def handler_403(request):
|
||||
raise Forbidden("Forbidden")
|
||||
|
||||
@app.route('/401')
|
||||
def handler_401(request):
|
||||
raise Unauthorized("Unauthorized")
|
||||
|
||||
@app.route('/401/basic')
|
||||
def handler_401_basic(request):
|
||||
raise Unauthorized("Unauthorized", "Basic", {"realm": "Sanic"})
|
||||
raise Unauthorized("Unauthorized", scheme="Basic", realm="Sanic")
|
||||
|
||||
@app.route('/401/digest')
|
||||
def handler_401_digest(request):
|
||||
challenge = {
|
||||
"realm": "Sanic",
|
||||
"qop": "auth, auth-int",
|
||||
"algorithm": "MD5",
|
||||
"nonce": "abcdef",
|
||||
"opaque": "zyxwvu",
|
||||
}
|
||||
raise Unauthorized("Unauthorized", "Digest", challenge)
|
||||
raise Unauthorized("Unauthorized",
|
||||
scheme="Digest",
|
||||
realm="Sanic",
|
||||
qop="auth, auth-int",
|
||||
algorithm="MD5",
|
||||
nonce="abcdef",
|
||||
opaque="zyxwvu")
|
||||
|
||||
@app.route('/401/bearer')
|
||||
def handler_401_bearer(request):
|
||||
raise Unauthorized("Unauthorized", "Bearer")
|
||||
raise Unauthorized("Unauthorized", scheme="Bearer")
|
||||
|
||||
@app.route('/invalid')
|
||||
def handler_invalid(request):
|
||||
raise InvalidUsage("OK")
|
||||
|
||||
@app.route('/abort/401')
|
||||
def handler_401_error(request):
|
||||
abort(401)
|
||||
|
||||
@app.route('/abort')
|
||||
def handler_invalid(request):
|
||||
def handler_500_error(request):
|
||||
abort(500)
|
||||
return text("OK")
|
||||
|
||||
|
@ -125,6 +132,9 @@ def test_forbidden_exception(exception_app):
|
|||
|
||||
def test_unauthorized_exception(exception_app):
|
||||
"""Test the built-in Unauthorized exception"""
|
||||
request, response = exception_app.test_client.get('/401')
|
||||
assert response.status == 401
|
||||
|
||||
request, response = exception_app.test_client.get('/401/basic')
|
||||
assert response.status == 401
|
||||
assert response.headers.get('WWW-Authenticate') is not None
|
||||
|
@ -176,7 +186,7 @@ def test_exception_in_exception_handler_debug_off(exception_app):
|
|||
assert response.body == b'An error occurred while handling an error'
|
||||
|
||||
|
||||
def test_exception_in_exception_handler_debug_off(exception_app):
|
||||
def test_exception_in_exception_handler_debug_on(exception_app):
|
||||
"""Test that an exception thrown in an error handler is handled"""
|
||||
request, response = exception_app.test_client.get(
|
||||
'/error_in_error_handler_handler',
|
||||
|
@ -187,5 +197,8 @@ def test_exception_in_exception_handler_debug_off(exception_app):
|
|||
|
||||
def test_abort(exception_app):
|
||||
"""Test the abort function"""
|
||||
request, response = exception_app.test_client.get('/abort/401')
|
||||
assert response.status == 401
|
||||
|
||||
request, response = exception_app.test_client.get('/abort')
|
||||
assert response.status == 500
|
||||
|
|
|
@ -24,7 +24,7 @@ def handler_3(request):
|
|||
|
||||
@exception_handler_app.route('/4')
|
||||
def handler_4(request):
|
||||
foo = bar
|
||||
foo = bar # noqa -- F821 undefined name 'bar' is done to throw exception
|
||||
return text(foo)
|
||||
|
||||
|
||||
|
|
269
tests/test_keep_alive_timeout.py
Normal file
269
tests/test_keep_alive_timeout.py
Normal file
|
@ -0,0 +1,269 @@
|
|||
from json import JSONDecodeError
|
||||
from sanic import Sanic
|
||||
import asyncio
|
||||
from asyncio import sleep as aio_sleep
|
||||
from sanic.response import text
|
||||
from sanic.config import Config
|
||||
from sanic import server
|
||||
import aiohttp
|
||||
from aiohttp import TCPConnector
|
||||
from sanic.testing import SanicTestClient, HOST, PORT
|
||||
|
||||
|
||||
class ReuseableTCPConnector(TCPConnector):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ReuseableTCPConnector, self).__init__(*args, **kwargs)
|
||||
self.old_proto = None
|
||||
|
||||
@asyncio.coroutine
|
||||
def connect(self, req):
|
||||
new_conn = yield from super(ReuseableTCPConnector, self)\
|
||||
.connect(req)
|
||||
if self.old_proto is not None:
|
||||
if self.old_proto != new_conn._protocol:
|
||||
raise RuntimeError(
|
||||
"We got a new connection, wanted the same one!")
|
||||
print(new_conn.__dict__)
|
||||
self.old_proto = new_conn._protocol
|
||||
return new_conn
|
||||
|
||||
|
||||
class ReuseableSanicTestClient(SanicTestClient):
|
||||
def __init__(self, app, loop=None):
|
||||
super(ReuseableSanicTestClient, self).__init__(app)
|
||||
if loop is None:
|
||||
loop = asyncio.get_event_loop()
|
||||
self._loop = loop
|
||||
self._server = None
|
||||
self._tcp_connector = None
|
||||
self._session = None
|
||||
|
||||
# Copied from SanicTestClient, but with some changes to reuse the
|
||||
# same loop for the same app.
|
||||
def _sanic_endpoint_test(
|
||||
self, method='get', uri='/', gather_request=True,
|
||||
debug=False, server_kwargs={},
|
||||
*request_args, **request_kwargs):
|
||||
loop = self._loop
|
||||
results = [None, None]
|
||||
exceptions = []
|
||||
do_kill_server = request_kwargs.pop('end_server', False)
|
||||
if gather_request:
|
||||
def _collect_request(request):
|
||||
if results[0] is None:
|
||||
results[0] = request
|
||||
|
||||
self.app.request_middleware.appendleft(_collect_request)
|
||||
|
||||
@self.app.listener('after_server_start')
|
||||
async def _collect_response(loop):
|
||||
try:
|
||||
if do_kill_server:
|
||||
request_kwargs['end_session'] = True
|
||||
response = await self._local_request(
|
||||
method, uri, *request_args,
|
||||
**request_kwargs)
|
||||
results[-1] = response
|
||||
except Exception as e2:
|
||||
import traceback
|
||||
traceback.print_tb(e2.__traceback__)
|
||||
exceptions.append(e2)
|
||||
#Don't stop here! self.app.stop()
|
||||
|
||||
if self._server is not None:
|
||||
_server = self._server
|
||||
else:
|
||||
_server_co = self.app.create_server(host=HOST, debug=debug,
|
||||
port=PORT, **server_kwargs)
|
||||
|
||||
server.trigger_events(
|
||||
self.app.listeners['before_server_start'], loop)
|
||||
|
||||
try:
|
||||
loop._stopping = False
|
||||
http_server = loop.run_until_complete(_server_co)
|
||||
except Exception as e1:
|
||||
import traceback
|
||||
traceback.print_tb(e1.__traceback__)
|
||||
raise e1
|
||||
self._server = _server = http_server
|
||||
server.trigger_events(
|
||||
self.app.listeners['after_server_start'], loop)
|
||||
self.app.listeners['after_server_start'].pop()
|
||||
|
||||
if do_kill_server:
|
||||
try:
|
||||
_server.close()
|
||||
self._server = None
|
||||
loop.run_until_complete(_server.wait_closed())
|
||||
self.app.stop()
|
||||
except Exception as e3:
|
||||
import traceback
|
||||
traceback.print_tb(e3.__traceback__)
|
||||
exceptions.append(e3)
|
||||
if exceptions:
|
||||
raise ValueError(
|
||||
"Exception during request: {}".format(exceptions))
|
||||
|
||||
if gather_request:
|
||||
self.app.request_middleware.pop()
|
||||
try:
|
||||
request, response = results
|
||||
return request, response
|
||||
except:
|
||||
raise ValueError(
|
||||
"Request and response object expected, got ({})".format(
|
||||
results))
|
||||
else:
|
||||
try:
|
||||
return results[-1]
|
||||
except:
|
||||
raise ValueError(
|
||||
"Request object expected, got ({})".format(results))
|
||||
|
||||
# Copied from SanicTestClient, but with some changes to reuse the
|
||||
# same TCPConnection and the sane ClientSession more than once.
|
||||
# Note, you cannot use the same session if you are in a _different_
|
||||
# loop, so the changes above are required too.
|
||||
async def _local_request(self, method, uri, cookies=None, *args,
|
||||
**kwargs):
|
||||
request_keepalive = kwargs.pop('request_keepalive',
|
||||
Config.KEEP_ALIVE_TIMEOUT)
|
||||
if uri.startswith(('http:', 'https:', 'ftp:', 'ftps://' '//')):
|
||||
url = uri
|
||||
else:
|
||||
url = 'http://{host}:{port}{uri}'.format(
|
||||
host=HOST, port=PORT, uri=uri)
|
||||
do_kill_session = kwargs.pop('end_session', False)
|
||||
if self._session:
|
||||
session = self._session
|
||||
else:
|
||||
if self._tcp_connector:
|
||||
conn = self._tcp_connector
|
||||
else:
|
||||
conn = ReuseableTCPConnector(verify_ssl=False,
|
||||
loop=self._loop,
|
||||
keepalive_timeout=
|
||||
request_keepalive)
|
||||
self._tcp_connector = conn
|
||||
session = aiohttp.ClientSession(cookies=cookies,
|
||||
connector=conn,
|
||||
loop=self._loop)
|
||||
self._session = session
|
||||
|
||||
async with getattr(session, method.lower())(
|
||||
url, *args, **kwargs) as response:
|
||||
try:
|
||||
response.text = await response.text()
|
||||
except UnicodeDecodeError:
|
||||
response.text = None
|
||||
|
||||
try:
|
||||
response.json = await response.json()
|
||||
except (JSONDecodeError,
|
||||
UnicodeDecodeError,
|
||||
aiohttp.ClientResponseError):
|
||||
response.json = None
|
||||
|
||||
response.body = await response.read()
|
||||
if do_kill_session:
|
||||
session.close()
|
||||
self._session = None
|
||||
return response
|
||||
|
||||
|
||||
Config.KEEP_ALIVE_TIMEOUT = 2
|
||||
Config.KEEP_ALIVE = True
|
||||
keep_alive_timeout_app_reuse = Sanic('test_ka_timeout_reuse')
|
||||
keep_alive_app_client_timeout = Sanic('test_ka_client_timeout')
|
||||
keep_alive_app_server_timeout = Sanic('test_ka_server_timeout')
|
||||
|
||||
|
||||
@keep_alive_timeout_app_reuse.route('/1')
|
||||
async def handler1(request):
|
||||
return text('OK')
|
||||
|
||||
|
||||
@keep_alive_app_client_timeout.route('/1')
|
||||
async def handler2(request):
|
||||
return text('OK')
|
||||
|
||||
|
||||
@keep_alive_app_server_timeout.route('/1')
|
||||
async def handler3(request):
|
||||
return text('OK')
|
||||
|
||||
|
||||
def test_keep_alive_timeout_reuse():
|
||||
"""If the server keep-alive timeout and client keep-alive timeout are
|
||||
both longer than the delay, the client _and_ server will successfully
|
||||
reuse the existing connection."""
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
client = ReuseableSanicTestClient(keep_alive_timeout_app_reuse, loop)
|
||||
headers = {
|
||||
'Connection': 'keep-alive'
|
||||
}
|
||||
request, response = client.get('/1', headers=headers)
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
loop.run_until_complete(aio_sleep(1))
|
||||
request, response = client.get('/1', end_server=True)
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
|
||||
|
||||
def test_keep_alive_client_timeout():
|
||||
"""If the server keep-alive timeout is longer than the client
|
||||
keep-alive timeout, client will try to create a new connection here."""
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
client = ReuseableSanicTestClient(keep_alive_app_client_timeout,
|
||||
loop)
|
||||
headers = {
|
||||
'Connection': 'keep-alive'
|
||||
}
|
||||
request, response = client.get('/1', headers=headers,
|
||||
request_keepalive=1)
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
loop.run_until_complete(aio_sleep(2))
|
||||
exception = None
|
||||
try:
|
||||
request, response = client.get('/1', end_server=True,
|
||||
request_keepalive=1)
|
||||
except ValueError as e:
|
||||
exception = e
|
||||
assert exception is not None
|
||||
assert isinstance(exception, ValueError)
|
||||
assert "got a new connection" in exception.args[0]
|
||||
|
||||
|
||||
def test_keep_alive_server_timeout():
|
||||
"""If the client keep-alive timeout is longer than the server
|
||||
keep-alive timeout, the client will either a 'Connection reset' error
|
||||
_or_ a new connection. Depending on how the event-loop handles the
|
||||
broken server connection."""
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
client = ReuseableSanicTestClient(keep_alive_app_server_timeout,
|
||||
loop)
|
||||
headers = {
|
||||
'Connection': 'keep-alive'
|
||||
}
|
||||
request, response = client.get('/1', headers=headers,
|
||||
request_keepalive=60)
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
loop.run_until_complete(aio_sleep(3))
|
||||
exception = None
|
||||
try:
|
||||
request, response = client.get('/1', request_keepalive=60,
|
||||
end_server=True)
|
||||
except ValueError as e:
|
||||
exception = e
|
||||
assert exception is not None
|
||||
assert isinstance(exception, ValueError)
|
||||
assert "Connection reset" in exception.args[0] or \
|
||||
"got a new connection" in exception.args[0]
|
||||
|
|
@ -1,15 +1,28 @@
|
|||
import asyncio
|
||||
import uuid
|
||||
from sanic.response import text
|
||||
from sanic import Sanic
|
||||
from io import StringIO
|
||||
import logging
|
||||
|
||||
from io import StringIO
|
||||
from importlib import reload
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock
|
||||
|
||||
import sanic
|
||||
from sanic.response import text
|
||||
from sanic.log import LOGGING_CONFIG_DEFAULTS
|
||||
from sanic import Sanic
|
||||
|
||||
|
||||
logging_format = '''module: %(module)s; \
|
||||
function: %(funcName)s(); \
|
||||
message: %(message)s'''
|
||||
|
||||
|
||||
def reset_logging():
|
||||
logging.shutdown()
|
||||
reload(logging)
|
||||
|
||||
|
||||
def test_log():
|
||||
log_stream = StringIO()
|
||||
for handler in logging.root.handlers[:]:
|
||||
|
@ -32,5 +45,63 @@ def test_log():
|
|||
log_text = log_stream.getvalue()
|
||||
assert rand_string in log_text
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_log()
|
||||
|
||||
def test_logging_defaults():
|
||||
reset_logging()
|
||||
app = Sanic("test_logging")
|
||||
|
||||
for fmt in [h.formatter for h in logging.getLogger('root').handlers]:
|
||||
assert fmt._fmt == LOGGING_CONFIG_DEFAULTS['formatters']['generic']['format']
|
||||
|
||||
for fmt in [h.formatter for h in logging.getLogger('sanic.error').handlers]:
|
||||
assert fmt._fmt == LOGGING_CONFIG_DEFAULTS['formatters']['generic']['format']
|
||||
|
||||
for fmt in [h.formatter for h in logging.getLogger('sanic.access').handlers]:
|
||||
assert fmt._fmt == LOGGING_CONFIG_DEFAULTS['formatters']['access']['format']
|
||||
|
||||
|
||||
def test_logging_pass_customer_logconfig():
|
||||
reset_logging()
|
||||
|
||||
modified_config = LOGGING_CONFIG_DEFAULTS
|
||||
modified_config['formatters']['generic']['format'] = '%(asctime)s - (%(name)s)[%(levelname)s]: %(message)s'
|
||||
modified_config['formatters']['access']['format'] = '%(asctime)s - (%(name)s)[%(levelname)s]: %(message)s'
|
||||
|
||||
app = Sanic("test_logging", log_config=modified_config)
|
||||
|
||||
for fmt in [h.formatter for h in logging.getLogger('root').handlers]:
|
||||
assert fmt._fmt == modified_config['formatters']['generic']['format']
|
||||
|
||||
for fmt in [h.formatter for h in logging.getLogger('sanic.error').handlers]:
|
||||
assert fmt._fmt == modified_config['formatters']['generic']['format']
|
||||
|
||||
for fmt in [h.formatter for h in logging.getLogger('sanic.access').handlers]:
|
||||
assert fmt._fmt == modified_config['formatters']['access']['format']
|
||||
|
||||
|
||||
@pytest.mark.parametrize('debug', (True, False, ))
|
||||
def test_log_connection_lost(debug, monkeypatch):
|
||||
""" Should not log Connection lost exception on non debug """
|
||||
app = Sanic('connection_lost')
|
||||
stream = StringIO()
|
||||
root = logging.getLogger('root')
|
||||
root.addHandler(logging.StreamHandler(stream))
|
||||
monkeypatch.setattr(sanic.server, 'logger', root)
|
||||
|
||||
@app.route('/conn_lost')
|
||||
async def conn_lost(request):
|
||||
response = text('Ok')
|
||||
response.output = Mock(side_effect=RuntimeError)
|
||||
return response
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
# catch ValueError: Exception during request
|
||||
app.test_client.get('/conn_lost', debug=debug)
|
||||
|
||||
log = stream.getvalue()
|
||||
|
||||
if debug:
|
||||
assert log.startswith(
|
||||
'Connection lost before response written @')
|
||||
else:
|
||||
assert 'Connection lost before response written @' not in log
|
||||
|
|
|
@ -59,7 +59,7 @@ def test_middleware_response_exception():
|
|||
result = {'status_code': None}
|
||||
|
||||
@app.middleware('response')
|
||||
async def process_response(reqest, response):
|
||||
async def process_response(request, response):
|
||||
result['status_code'] = response.status
|
||||
return response
|
||||
|
||||
|
|
388
tests/test_named_routes.py
Normal file
388
tests/test_named_routes.py
Normal file
|
@ -0,0 +1,388 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import asyncio
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.response import text
|
||||
from sanic.exceptions import URLBuildError
|
||||
from sanic.constants import HTTP_METHODS
|
||||
|
||||
|
||||
# ------------------------------------------------------------ #
|
||||
# UTF-8
|
||||
# ------------------------------------------------------------ #
|
||||
|
||||
@pytest.mark.parametrize('method', HTTP_METHODS)
|
||||
def test_versioned_named_routes_get(method):
|
||||
app = Sanic('test_shorhand_routes_get')
|
||||
bp = Blueprint('test_bp', url_prefix='/bp')
|
||||
|
||||
method = method.lower()
|
||||
route_name = 'route_{}'.format(method)
|
||||
route_name2 = 'route2_{}'.format(method)
|
||||
|
||||
func = getattr(app, method)
|
||||
if callable(func):
|
||||
@func('/{}'.format(method), version=1, name=route_name)
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
else:
|
||||
print(func)
|
||||
raise
|
||||
|
||||
func = getattr(bp, method)
|
||||
if callable(func):
|
||||
@func('/{}'.format(method), version=1, name=route_name2)
|
||||
def handler2(request):
|
||||
return text('OK')
|
||||
|
||||
else:
|
||||
print(func)
|
||||
raise
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
assert app.router.routes_all['/v1/{}'.format(method)].name == route_name
|
||||
|
||||
route = app.router.routes_all['/v1/bp/{}'.format(method)]
|
||||
assert route.name == 'test_bp.{}'.format(route_name2)
|
||||
|
||||
assert app.url_for(route_name) == '/v1/{}'.format(method)
|
||||
url = app.url_for('test_bp.{}'.format(route_name2))
|
||||
assert url == '/v1/bp/{}'.format(method)
|
||||
with pytest.raises(URLBuildError):
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_shorthand_default_routes_get():
|
||||
app = Sanic('test_shorhand_routes_get')
|
||||
|
||||
@app.get('/get')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
assert app.router.routes_all['/get'].name == 'handler'
|
||||
assert app.url_for('handler') == '/get'
|
||||
|
||||
|
||||
def test_shorthand_named_routes_get():
|
||||
app = Sanic('test_shorhand_routes_get')
|
||||
bp = Blueprint('test_bp', url_prefix='/bp')
|
||||
|
||||
@app.get('/get', name='route_get')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
@bp.get('/get', name='route_bp')
|
||||
def handler2(request):
|
||||
return text('Blueprint')
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
assert app.router.routes_all['/get'].name == 'route_get'
|
||||
assert app.url_for('route_get') == '/get'
|
||||
with pytest.raises(URLBuildError):
|
||||
app.url_for('handler')
|
||||
|
||||
assert app.router.routes_all['/bp/get'].name == 'test_bp.route_bp'
|
||||
assert app.url_for('test_bp.route_bp') == '/bp/get'
|
||||
with pytest.raises(URLBuildError):
|
||||
app.url_for('test_bp.handler2')
|
||||
|
||||
|
||||
def test_shorthand_named_routes_post():
|
||||
app = Sanic('test_shorhand_routes_post')
|
||||
|
||||
@app.post('/post', name='route_name')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
assert app.router.routes_all['/post'].name == 'route_name'
|
||||
assert app.url_for('route_name') == '/post'
|
||||
with pytest.raises(URLBuildError):
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_shorthand_named_routes_put():
|
||||
app = Sanic('test_shorhand_routes_put')
|
||||
|
||||
@app.put('/put', name='route_put')
|
||||
def handler(request):
|
||||
assert request.stream is None
|
||||
return text('OK')
|
||||
|
||||
assert app.is_request_stream is False
|
||||
assert app.router.routes_all['/put'].name == 'route_put'
|
||||
assert app.url_for('route_put') == '/put'
|
||||
with pytest.raises(URLBuildError):
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_shorthand_named_routes_delete():
|
||||
app = Sanic('test_shorhand_routes_delete')
|
||||
|
||||
@app.delete('/delete', name='route_delete')
|
||||
def handler(request):
|
||||
assert request.stream is None
|
||||
return text('OK')
|
||||
|
||||
assert app.is_request_stream is False
|
||||
assert app.router.routes_all['/delete'].name == 'route_delete'
|
||||
assert app.url_for('route_delete') == '/delete'
|
||||
with pytest.raises(URLBuildError):
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_shorthand_named_routes_patch():
|
||||
app = Sanic('test_shorhand_routes_patch')
|
||||
|
||||
@app.patch('/patch', name='route_patch')
|
||||
def handler(request):
|
||||
assert request.stream is None
|
||||
return text('OK')
|
||||
|
||||
assert app.is_request_stream is False
|
||||
assert app.router.routes_all['/patch'].name == 'route_patch'
|
||||
assert app.url_for('route_patch') == '/patch'
|
||||
with pytest.raises(URLBuildError):
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_shorthand_named_routes_head():
|
||||
app = Sanic('test_shorhand_routes_head')
|
||||
|
||||
@app.head('/head', name='route_head')
|
||||
def handler(request):
|
||||
assert request.stream is None
|
||||
return text('OK')
|
||||
|
||||
assert app.is_request_stream is False
|
||||
assert app.router.routes_all['/head'].name == 'route_head'
|
||||
assert app.url_for('route_head') == '/head'
|
||||
with pytest.raises(URLBuildError):
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_shorthand_named_routes_options():
|
||||
app = Sanic('test_shorhand_routes_options')
|
||||
|
||||
@app.options('/options', name='route_options')
|
||||
def handler(request):
|
||||
assert request.stream is None
|
||||
return text('OK')
|
||||
|
||||
assert app.is_request_stream is False
|
||||
assert app.router.routes_all['/options'].name == 'route_options'
|
||||
assert app.url_for('route_options') == '/options'
|
||||
with pytest.raises(URLBuildError):
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_named_static_routes():
|
||||
app = Sanic('test_dynamic_route')
|
||||
|
||||
@app.route('/test', name='route_test')
|
||||
async def handler1(request):
|
||||
return text('OK1')
|
||||
|
||||
@app.route('/pizazz', name='route_pizazz')
|
||||
async def handler2(request):
|
||||
return text('OK2')
|
||||
|
||||
assert app.router.routes_all['/test'].name == 'route_test'
|
||||
assert app.router.routes_static['/test'].name == 'route_test'
|
||||
assert app.url_for('route_test') == '/test'
|
||||
with pytest.raises(URLBuildError):
|
||||
app.url_for('handler1')
|
||||
|
||||
assert app.router.routes_all['/pizazz'].name == 'route_pizazz'
|
||||
assert app.router.routes_static['/pizazz'].name == 'route_pizazz'
|
||||
assert app.url_for('route_pizazz') == '/pizazz'
|
||||
with pytest.raises(URLBuildError):
|
||||
app.url_for('handler2')
|
||||
|
||||
|
||||
def test_named_dynamic_route():
|
||||
app = Sanic('test_dynamic_route')
|
||||
|
||||
results = []
|
||||
|
||||
@app.route('/folder/<name>', name='route_dynamic')
|
||||
async def handler(request, name):
|
||||
results.append(name)
|
||||
return text('OK')
|
||||
|
||||
assert app.router.routes_all['/folder/<name>'].name == 'route_dynamic'
|
||||
assert app.url_for('route_dynamic', name='test') == '/folder/test'
|
||||
with pytest.raises(URLBuildError):
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_dynamic_named_route_regex():
|
||||
app = Sanic('test_dynamic_route_regex')
|
||||
|
||||
@app.route('/folder/<folder_id:[A-Za-z0-9]{0,4}>', name='route_re')
|
||||
async def handler(request, folder_id):
|
||||
return text('OK')
|
||||
|
||||
route = app.router.routes_all['/folder/<folder_id:[A-Za-z0-9]{0,4}>']
|
||||
assert route.name == 'route_re'
|
||||
assert app.url_for('route_re', folder_id='test') == '/folder/test'
|
||||
with pytest.raises(URLBuildError):
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_dynamic_named_route_path():
|
||||
app = Sanic('test_dynamic_route_path')
|
||||
|
||||
@app.route('/<path:path>/info', name='route_dynamic_path')
|
||||
async def handler(request, path):
|
||||
return text('OK')
|
||||
|
||||
route = app.router.routes_all['/<path:path>/info']
|
||||
assert route.name == 'route_dynamic_path'
|
||||
assert app.url_for('route_dynamic_path', path='path/1') == '/path/1/info'
|
||||
with pytest.raises(URLBuildError):
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_dynamic_named_route_unhashable():
|
||||
app = Sanic('test_dynamic_route_unhashable')
|
||||
|
||||
@app.route('/folder/<unhashable:[A-Za-z0-9/]+>/end/',
|
||||
name='route_unhashable')
|
||||
async def handler(request, unhashable):
|
||||
return text('OK')
|
||||
|
||||
route = app.router.routes_all['/folder/<unhashable:[A-Za-z0-9/]+>/end/']
|
||||
assert route.name == 'route_unhashable'
|
||||
url = app.url_for('route_unhashable', unhashable='test/asdf')
|
||||
assert url == '/folder/test/asdf/end'
|
||||
with pytest.raises(URLBuildError):
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_websocket_named_route():
|
||||
app = Sanic('test_websocket_route')
|
||||
ev = asyncio.Event()
|
||||
|
||||
@app.websocket('/ws', name='route_ws')
|
||||
async def handler(request, ws):
|
||||
assert ws.subprotocol is None
|
||||
ev.set()
|
||||
|
||||
assert app.router.routes_all['/ws'].name == 'route_ws'
|
||||
assert app.url_for('route_ws') == '/ws'
|
||||
with pytest.raises(URLBuildError):
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_websocket_named_route_with_subprotocols():
|
||||
app = Sanic('test_websocket_route')
|
||||
results = []
|
||||
|
||||
@app.websocket('/ws', subprotocols=['foo', 'bar'], name='route_ws')
|
||||
async def handler(request, ws):
|
||||
results.append(ws.subprotocol)
|
||||
|
||||
assert app.router.routes_all['/ws'].name == 'route_ws'
|
||||
assert app.url_for('route_ws') == '/ws'
|
||||
with pytest.raises(URLBuildError):
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_static_add_named_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', name='route_test')
|
||||
app.add_route(handler2, '/test2', name='route_test2')
|
||||
|
||||
assert app.router.routes_all['/test'].name == 'route_test'
|
||||
assert app.router.routes_static['/test'].name == 'route_test'
|
||||
assert app.url_for('route_test') == '/test'
|
||||
with pytest.raises(URLBuildError):
|
||||
app.url_for('handler1')
|
||||
|
||||
assert app.router.routes_all['/test2'].name == 'route_test2'
|
||||
assert app.router.routes_static['/test2'].name == 'route_test2'
|
||||
assert app.url_for('route_test2') == '/test2'
|
||||
with pytest.raises(URLBuildError):
|
||||
app.url_for('handler2')
|
||||
|
||||
|
||||
def test_dynamic_add_named_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>', name='route_dynamic')
|
||||
assert app.router.routes_all['/folder/<name>'].name == 'route_dynamic'
|
||||
assert app.url_for('route_dynamic', name='test') == '/folder/test'
|
||||
with pytest.raises(URLBuildError):
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_dynamic_add_named_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/',
|
||||
name='route_unhashable')
|
||||
route = app.router.routes_all['/folder/<unhashable:[A-Za-z0-9/]+>/end/']
|
||||
assert route.name == 'route_unhashable'
|
||||
url = app.url_for('route_unhashable', unhashable='folder1')
|
||||
assert url == '/folder/folder1/end'
|
||||
with pytest.raises(URLBuildError):
|
||||
app.url_for('handler')
|
||||
|
||||
|
||||
def test_overload_routes():
|
||||
app = Sanic('test_dynamic_route')
|
||||
|
||||
@app.route('/overload', methods=['GET'], name='route_first')
|
||||
async def handler1(request):
|
||||
return text('OK1')
|
||||
|
||||
@app.route('/overload', methods=['POST', 'PUT'], name='route_second')
|
||||
async def handler1(request):
|
||||
return text('OK2')
|
||||
|
||||
request, response = app.test_client.get(app.url_for('route_first'))
|
||||
assert response.text == 'OK1'
|
||||
|
||||
request, response = app.test_client.post(app.url_for('route_first'))
|
||||
assert response.text == 'OK2'
|
||||
|
||||
request, response = app.test_client.put(app.url_for('route_first'))
|
||||
assert response.text == 'OK2'
|
||||
|
||||
request, response = app.test_client.get(app.url_for('route_second'))
|
||||
assert response.text == 'OK1'
|
||||
|
||||
request, response = app.test_client.post(app.url_for('route_second'))
|
||||
assert response.text == 'OK2'
|
||||
|
||||
request, response = app.test_client.put(app.url_for('route_second'))
|
||||
assert response.text == 'OK2'
|
||||
|
||||
assert app.router.routes_all['/overload'].name == 'route_first'
|
||||
with pytest.raises(URLBuildError):
|
||||
app.url_for('handler1')
|
||||
|
||||
assert app.url_for('route_first') == '/overload'
|
||||
assert app.url_for('route_second') == app.url_for('route_first')
|
|
@ -1,38 +1,163 @@
|
|||
from json import JSONDecodeError
|
||||
|
||||
from sanic import Sanic
|
||||
import asyncio
|
||||
from sanic.response import text
|
||||
from sanic.exceptions import RequestTimeout
|
||||
from sanic.config import Config
|
||||
import aiohttp
|
||||
from aiohttp import TCPConnector
|
||||
from sanic.testing import SanicTestClient, HOST, PORT
|
||||
|
||||
Config.REQUEST_TIMEOUT = 1
|
||||
request_timeout_app = Sanic('test_request_timeout')
|
||||
|
||||
class DelayableTCPConnector(TCPConnector):
|
||||
|
||||
class RequestContextManager(object):
|
||||
def __new__(cls, req, delay):
|
||||
cls = super(DelayableTCPConnector.RequestContextManager, cls).\
|
||||
__new__(cls)
|
||||
cls.req = req
|
||||
cls.send_task = None
|
||||
cls.resp = None
|
||||
cls.orig_send = getattr(req, 'send')
|
||||
cls.orig_start = None
|
||||
cls.delay = delay
|
||||
cls._acting_as = req
|
||||
return cls
|
||||
|
||||
def __getattr__(self, item):
|
||||
acting_as = self._acting_as
|
||||
return getattr(acting_as, item)
|
||||
|
||||
@asyncio.coroutine
|
||||
def start(self, connection, read_until_eof=False):
|
||||
if self.send_task is None:
|
||||
raise RuntimeError("do a send() before you do a start()")
|
||||
resp = yield from self.send_task
|
||||
self.send_task = None
|
||||
self.resp = resp
|
||||
self._acting_as = self.resp
|
||||
self.orig_start = getattr(resp, 'start')
|
||||
|
||||
try:
|
||||
ret = yield from self.orig_start(connection,
|
||||
read_until_eof)
|
||||
except Exception as e:
|
||||
raise e
|
||||
return ret
|
||||
|
||||
def close(self):
|
||||
if self.resp is not None:
|
||||
self.resp.close()
|
||||
if self.send_task is not None:
|
||||
self.send_task.cancel()
|
||||
|
||||
@asyncio.coroutine
|
||||
def delayed_send(self, *args, **kwargs):
|
||||
req = self.req
|
||||
if self.delay and self.delay > 0:
|
||||
#sync_sleep(self.delay)
|
||||
_ = yield from asyncio.sleep(self.delay)
|
||||
t = req.loop.time()
|
||||
print("sending at {}".format(t), flush=True)
|
||||
conn = next(iter(args)) # first arg is connection
|
||||
try:
|
||||
delayed_resp = self.orig_send(*args, **kwargs)
|
||||
except Exception as e:
|
||||
return aiohttp.ClientResponse(req.method, req.url)
|
||||
return delayed_resp
|
||||
|
||||
def send(self, *args, **kwargs):
|
||||
gen = self.delayed_send(*args, **kwargs)
|
||||
task = self.req.loop.create_task(gen)
|
||||
self.send_task = task
|
||||
self._acting_as = task
|
||||
return self
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
_post_connect_delay = kwargs.pop('post_connect_delay', 0)
|
||||
_pre_request_delay = kwargs.pop('pre_request_delay', 0)
|
||||
super(DelayableTCPConnector, self).__init__(*args, **kwargs)
|
||||
self._post_connect_delay = _post_connect_delay
|
||||
self._pre_request_delay = _pre_request_delay
|
||||
|
||||
@asyncio.coroutine
|
||||
def connect(self, req):
|
||||
d_req = DelayableTCPConnector.\
|
||||
RequestContextManager(req, self._pre_request_delay)
|
||||
conn = yield from super(DelayableTCPConnector, self).connect(req)
|
||||
if self._post_connect_delay and self._post_connect_delay > 0:
|
||||
_ = yield from asyncio.sleep(self._post_connect_delay,
|
||||
loop=self._loop)
|
||||
req.send = d_req.send
|
||||
t = req.loop.time()
|
||||
print("Connected at {}".format(t), flush=True)
|
||||
return conn
|
||||
|
||||
|
||||
class DelayableSanicTestClient(SanicTestClient):
|
||||
def __init__(self, app, loop, request_delay=1):
|
||||
super(DelayableSanicTestClient, self).__init__(app)
|
||||
self._request_delay = request_delay
|
||||
self._loop = None
|
||||
|
||||
async def _local_request(self, method, uri, cookies=None, *args,
|
||||
**kwargs):
|
||||
if self._loop is None:
|
||||
self._loop = asyncio.get_event_loop()
|
||||
if uri.startswith(('http:', 'https:', 'ftp:', 'ftps://' '//')):
|
||||
url = uri
|
||||
else:
|
||||
url = 'http://{host}:{port}{uri}'.format(
|
||||
host=HOST, port=PORT, uri=uri)
|
||||
conn = DelayableTCPConnector(pre_request_delay=self._request_delay,
|
||||
verify_ssl=False, loop=self._loop)
|
||||
async with aiohttp.ClientSession(cookies=cookies, connector=conn,
|
||||
loop=self._loop) as session:
|
||||
# Insert a delay after creating the connection
|
||||
# But before sending the request.
|
||||
|
||||
async with getattr(session, method.lower())(
|
||||
url, *args, **kwargs) as response:
|
||||
try:
|
||||
response.text = await response.text()
|
||||
except UnicodeDecodeError:
|
||||
response.text = None
|
||||
|
||||
try:
|
||||
response.json = await response.json()
|
||||
except (JSONDecodeError,
|
||||
UnicodeDecodeError,
|
||||
aiohttp.ClientResponseError):
|
||||
response.json = None
|
||||
|
||||
response.body = await response.read()
|
||||
return response
|
||||
|
||||
|
||||
Config.REQUEST_TIMEOUT = 2
|
||||
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 = request_timeout_app.test_client.get('/1')
|
||||
assert response.status == 408
|
||||
assert response.text == 'Request Timeout from error_handler.'
|
||||
request_no_timeout_app = Sanic('test_request_no_timeout')
|
||||
|
||||
|
||||
@request_timeout_default_app.route('/1')
|
||||
async def handler_2(request):
|
||||
await asyncio.sleep(2)
|
||||
async def handler1(request):
|
||||
return text('OK')
|
||||
|
||||
|
||||
@request_no_timeout_app.route('/1')
|
||||
async def handler2(request):
|
||||
return text('OK')
|
||||
|
||||
|
||||
def test_default_server_error_request_timeout():
|
||||
request, response = request_timeout_default_app.test_client.get('/1')
|
||||
client = DelayableSanicTestClient(request_timeout_default_app, None, 3)
|
||||
request, response = client.get('/1')
|
||||
assert response.status == 408
|
||||
assert response.text == 'Error: Request Timeout'
|
||||
|
||||
|
||||
def test_default_server_error_request_dont_timeout():
|
||||
client = DelayableSanicTestClient(request_no_timeout_app, None, 1)
|
||||
request, response = client.get('/1')
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
|
|
|
@ -27,6 +27,16 @@ def test_sync():
|
|||
|
||||
assert response.text == 'Hello'
|
||||
|
||||
def test_remote_address():
|
||||
app = Sanic('test_text')
|
||||
|
||||
@app.route('/')
|
||||
def handler(request):
|
||||
return text("{}".format(request.ip))
|
||||
|
||||
request, response = app.test_client.get('/')
|
||||
|
||||
assert response.text == '127.0.0.1'
|
||||
|
||||
def test_text():
|
||||
app = Sanic('test_text')
|
||||
|
|
|
@ -35,6 +35,25 @@ async def sample_streaming_fn(response):
|
|||
await asyncio.sleep(.001)
|
||||
response.write('bar')
|
||||
|
||||
def test_method_not_allowed():
|
||||
app = Sanic('method_not_allowed')
|
||||
|
||||
@app.get('/')
|
||||
async def test(request):
|
||||
return response.json({'hello': 'world'})
|
||||
|
||||
request, response = app.test_client.head('/')
|
||||
assert response.headers['Allow']== 'GET'
|
||||
|
||||
@app.post('/')
|
||||
async def test(request):
|
||||
return response.json({'hello': 'world'})
|
||||
|
||||
request, response = app.test_client.head('/')
|
||||
assert response.status == 405
|
||||
assert set(response.headers['Allow'].split(', ')) == set(['GET', 'POST'])
|
||||
assert response.headers['Content-Length'] == '0'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def json_app():
|
||||
|
@ -149,7 +168,22 @@ def test_file_response(file_name, static_file_directory):
|
|||
request, response = app.test_client.get('/files/{}'.format(file_name))
|
||||
assert response.status == 200
|
||||
assert response.body == get_file_content(static_file_directory, file_name)
|
||||
assert 'Content-Disposition' not in response.headers
|
||||
|
||||
@pytest.mark.parametrize('source,dest', [
|
||||
('test.file', 'my_file.txt'), ('decode me.txt', 'readme.md'), ('python.png', 'logo.png')])
|
||||
def test_file_response_custom_filename(source, dest, static_file_directory):
|
||||
app = Sanic('test_file_helper')
|
||||
@app.route('/files/<filename>', methods=['GET'])
|
||||
def file_route(request, filename):
|
||||
file_path = os.path.join(static_file_directory, filename)
|
||||
file_path = os.path.abspath(unquote(file_path))
|
||||
return file(file_path, filename=dest)
|
||||
|
||||
request, response = app.test_client.get('/files/{}'.format(source))
|
||||
assert response.status == 200
|
||||
assert response.body == get_file_content(static_file_directory, source)
|
||||
assert response.headers['Content-Disposition'] == 'attachment; filename="{}"'.format(dest)
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_file_head_response(file_name, static_file_directory):
|
||||
|
@ -191,7 +225,22 @@ def test_file_stream_response(file_name, static_file_directory):
|
|||
request, response = app.test_client.get('/files/{}'.format(file_name))
|
||||
assert response.status == 200
|
||||
assert response.body == get_file_content(static_file_directory, file_name)
|
||||
assert 'Content-Disposition' not in response.headers
|
||||
|
||||
@pytest.mark.parametrize('source,dest', [
|
||||
('test.file', 'my_file.txt'), ('decode me.txt', 'readme.md'), ('python.png', 'logo.png')])
|
||||
def test_file_stream_response_custom_filename(source, dest, static_file_directory):
|
||||
app = Sanic('test_file_helper')
|
||||
@app.route('/files/<filename>', methods=['GET'])
|
||||
def file_route(request, filename):
|
||||
file_path = os.path.join(static_file_directory, filename)
|
||||
file_path = os.path.abspath(unquote(file_path))
|
||||
return file_stream(file_path, chunk_size=32, filename=dest)
|
||||
|
||||
request, response = app.test_client.get('/files/{}'.format(source))
|
||||
assert response.status == 200
|
||||
assert response.body == get_file_content(static_file_directory, source)
|
||||
assert response.headers['Content-Disposition'] == 'attachment; filename="{}"'.format(dest)
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_file_stream_head_response(file_name, static_file_directory):
|
||||
|
|
38
tests/test_response_timeout.py
Normal file
38
tests/test_response_timeout.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
from sanic import Sanic
|
||||
import asyncio
|
||||
from sanic.response import text
|
||||
from sanic.exceptions import ServiceUnavailable
|
||||
from sanic.config import Config
|
||||
|
||||
Config.RESPONSE_TIMEOUT = 1
|
||||
response_timeout_app = Sanic('test_response_timeout')
|
||||
response_timeout_default_app = Sanic('test_response_timeout_default')
|
||||
|
||||
|
||||
@response_timeout_app.route('/1')
|
||||
async def handler_1(request):
|
||||
await asyncio.sleep(2)
|
||||
return text('OK')
|
||||
|
||||
|
||||
@response_timeout_app.exception(ServiceUnavailable)
|
||||
def handler_exception(request, exception):
|
||||
return text('Response Timeout from error_handler.', 503)
|
||||
|
||||
|
||||
def test_server_error_response_timeout():
|
||||
request, response = response_timeout_app.test_client.get('/1')
|
||||
assert response.status == 503
|
||||
assert response.text == 'Response Timeout from error_handler.'
|
||||
|
||||
|
||||
@response_timeout_default_app.route('/1')
|
||||
async def handler_2(request):
|
||||
await asyncio.sleep(2)
|
||||
return text('OK')
|
||||
|
||||
|
||||
def test_default_server_error_response_timeout():
|
||||
request, response = response_timeout_default_app.test_client.get('/1')
|
||||
assert response.status == 503
|
||||
assert response.text == 'Error: Response Timeout'
|
|
@ -44,6 +44,24 @@ def test_shorthand_routes_get():
|
|||
request, response = app.test_client.post('/get')
|
||||
assert response.status == 405
|
||||
|
||||
def test_shorthand_routes_multiple():
|
||||
app = Sanic('test_shorthand_routes_multiple')
|
||||
|
||||
@app.get('/get')
|
||||
def get_handler(request):
|
||||
return text('OK')
|
||||
|
||||
@app.options('/get')
|
||||
def options_handler(request):
|
||||
return text('')
|
||||
|
||||
request, response = app.test_client.get('/get/')
|
||||
assert response.status == 200
|
||||
assert response.text == 'OK'
|
||||
|
||||
request, response = app.test_client.options('/get/')
|
||||
assert response.status == 200
|
||||
|
||||
def test_route_strict_slash():
|
||||
app = Sanic('test_route_strict_slash')
|
||||
|
||||
|
@ -71,6 +89,46 @@ def test_route_strict_slash():
|
|||
request, response = app.test_client.post('/post')
|
||||
assert response.status == 404
|
||||
|
||||
def test_route_invalid_parameter_syntax():
|
||||
with pytest.raises(ValueError):
|
||||
app = Sanic('test_route_invalid_param_syntax')
|
||||
|
||||
@app.get('/get/<:string>', strict_slashes=True)
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = app.test_client.get('/get')
|
||||
|
||||
def test_route_strict_slash_default_value():
|
||||
app = Sanic('test_route_strict_slash', strict_slashes=True)
|
||||
|
||||
@app.get('/get')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = app.test_client.get('/get/')
|
||||
assert response.status == 404
|
||||
|
||||
def test_route_strict_slash_without_passing_default_value():
|
||||
app = Sanic('test_route_strict_slash')
|
||||
|
||||
@app.get('/get')
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = app.test_client.get('/get/')
|
||||
assert response.text == 'OK'
|
||||
|
||||
def test_route_strict_slash_default_value_can_be_overwritten():
|
||||
app = Sanic('test_route_strict_slash', strict_slashes=True)
|
||||
|
||||
@app.get('/get', strict_slashes=False)
|
||||
def handler(request):
|
||||
return text('OK')
|
||||
|
||||
request, response = app.test_client.get('/get/')
|
||||
assert response.text == 'OK'
|
||||
|
||||
def test_route_optional_slash():
|
||||
app = Sanic('test_route_optional_slash')
|
||||
|
||||
|
@ -341,6 +399,7 @@ def test_websocket_route():
|
|||
|
||||
@app.websocket('/ws')
|
||||
async def handler(request, ws):
|
||||
assert ws.subprotocol is None
|
||||
ev.set()
|
||||
|
||||
request, response = app.test_client.get('/ws', headers={
|
||||
|
@ -352,6 +411,48 @@ def test_websocket_route():
|
|||
assert ev.is_set()
|
||||
|
||||
|
||||
def test_websocket_route_with_subprotocols():
|
||||
app = Sanic('test_websocket_route')
|
||||
results = []
|
||||
|
||||
@app.websocket('/ws', subprotocols=['foo', 'bar'])
|
||||
async def handler(request, ws):
|
||||
results.append(ws.subprotocol)
|
||||
|
||||
request, response = app.test_client.get('/ws', headers={
|
||||
'Upgrade': 'websocket',
|
||||
'Connection': 'upgrade',
|
||||
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
|
||||
'Sec-WebSocket-Version': '13',
|
||||
'Sec-WebSocket-Protocol': 'bar'})
|
||||
assert response.status == 101
|
||||
|
||||
request, response = app.test_client.get('/ws', headers={
|
||||
'Upgrade': 'websocket',
|
||||
'Connection': 'upgrade',
|
||||
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
|
||||
'Sec-WebSocket-Version': '13',
|
||||
'Sec-WebSocket-Protocol': 'bar, foo'})
|
||||
assert response.status == 101
|
||||
|
||||
request, response = app.test_client.get('/ws', headers={
|
||||
'Upgrade': 'websocket',
|
||||
'Connection': 'upgrade',
|
||||
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
|
||||
'Sec-WebSocket-Version': '13',
|
||||
'Sec-WebSocket-Protocol': 'baz'})
|
||||
assert response.status == 101
|
||||
|
||||
request, response = app.test_client.get('/ws', headers={
|
||||
'Upgrade': 'websocket',
|
||||
'Connection': 'upgrade',
|
||||
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
|
||||
'Sec-WebSocket-Version': '13'})
|
||||
assert response.status == 101
|
||||
|
||||
assert results == ['bar', 'bar', None, None]
|
||||
|
||||
|
||||
def test_route_duplicate():
|
||||
app = Sanic('test_route_duplicate')
|
||||
|
||||
|
@ -671,6 +772,7 @@ def test_remove_route_without_clean_cache():
|
|||
assert response.status == 200
|
||||
|
||||
app.remove_route('/test', clean_cache=True)
|
||||
app.remove_route('/test/', clean_cache=True)
|
||||
|
||||
request, response = app.test_client.get('/test')
|
||||
assert response.status == 404
|
||||
|
|
|
@ -59,3 +59,20 @@ def test_all_listeners():
|
|||
start_stop_app(random_name_app)
|
||||
for listener_name in AVAILABLE_LISTENERS:
|
||||
assert random_name_app.name + listener_name == output.pop()
|
||||
|
||||
|
||||
async def test_trigger_before_events_create_server():
|
||||
|
||||
class MySanicDb:
|
||||
pass
|
||||
|
||||
app = Sanic("test_sanic_app")
|
||||
|
||||
@app.listener('before_server_start')
|
||||
async def init_db(app, loop):
|
||||
app.db = MySanicDb()
|
||||
|
||||
await app.create_server()
|
||||
|
||||
assert hasattr(app, "db")
|
||||
assert isinstance(app.db, MySanicDb)
|
||||
|
|
|
@ -161,3 +161,20 @@ def test_static_content_range_error(file_name, static_file_directory):
|
|||
assert 'Content-Range' in response.headers
|
||||
assert response.headers['Content-Range'] == "bytes */%s" % (
|
||||
len(get_file_content(static_file_directory, file_name)),)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png'])
|
||||
def test_static_file_specified_host(static_file_directory, file_name):
|
||||
app = Sanic('test_static')
|
||||
app.static(
|
||||
'/testing.file',
|
||||
get_file_path(static_file_directory, file_name),
|
||||
host="www.example.com"
|
||||
)
|
||||
|
||||
headers = {"Host": "www.example.com"}
|
||||
request, response = app.test_client.get('/testing.file', headers=headers)
|
||||
assert response.status == 200
|
||||
assert response.body == get_file_content(static_file_directory, file_name)
|
||||
request, response = app.test_client.get('/testing.file')
|
||||
assert response.status == 404
|
||||
|
|
|
@ -5,7 +5,7 @@ from sanic import Sanic
|
|||
from sanic.response import text
|
||||
from sanic.views import HTTPMethodView
|
||||
from sanic.blueprints import Blueprint
|
||||
from sanic.testing import PORT as test_port
|
||||
from sanic.testing import PORT as test_port, HOST as test_host
|
||||
from sanic.exceptions import URLBuildError
|
||||
|
||||
import string
|
||||
|
@ -15,8 +15,11 @@ URL_FOR_VALUE1 = '/myurl?arg1=v1&arg1=v2'
|
|||
URL_FOR_ARGS2 = dict(arg1=['v1', 'v2'], _anchor='anchor')
|
||||
URL_FOR_VALUE2 = '/myurl?arg1=v1&arg1=v2#anchor'
|
||||
URL_FOR_ARGS3 = dict(arg1='v1', _anchor='anchor', _scheme='http',
|
||||
_server='localhost:{}'.format(test_port), _external=True)
|
||||
URL_FOR_VALUE3 = 'http://localhost:{}/myurl?arg1=v1#anchor'.format(test_port)
|
||||
_server='{}:{}'.format(test_host, test_port), _external=True)
|
||||
URL_FOR_VALUE3 = 'http://{}:{}/myurl?arg1=v1#anchor'.format(test_host, test_port)
|
||||
URL_FOR_ARGS4 = dict(arg1='v1', _anchor='anchor', _external=True,
|
||||
_server='http://{}:{}'.format(test_host, test_port),)
|
||||
URL_FOR_VALUE4 = 'http://{}:{}/myurl?arg1=v1#anchor'.format(test_host, test_port)
|
||||
|
||||
|
||||
def _generate_handlers_from_names(app, l):
|
||||
|
@ -49,7 +52,8 @@ def test_simple_url_for_getting(simple_app):
|
|||
@pytest.mark.parametrize('args,url',
|
||||
[(URL_FOR_ARGS1, URL_FOR_VALUE1),
|
||||
(URL_FOR_ARGS2, URL_FOR_VALUE2),
|
||||
(URL_FOR_ARGS3, URL_FOR_VALUE3)])
|
||||
(URL_FOR_ARGS3, URL_FOR_VALUE3),
|
||||
(URL_FOR_ARGS4, URL_FOR_VALUE4)])
|
||||
def test_simple_url_for_getting_with_more_params(args, url):
|
||||
app = Sanic('more_url_build')
|
||||
|
||||
|
|
446
tests/test_url_for_static.py
Normal file
446
tests/test_url_for_static.py
Normal file
|
@ -0,0 +1,446 @@
|
|||
import inspect
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from sanic import Sanic
|
||||
from sanic.blueprints import Blueprint
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
def get_file_path(static_file_directory, file_name):
|
||||
return os.path.join(static_file_directory, file_name)
|
||||
|
||||
|
||||
def get_file_content(static_file_directory, file_name):
|
||||
"""The content of the static file to check"""
|
||||
with open(get_file_path(static_file_directory, file_name), 'rb') as file:
|
||||
return file.read()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png'])
|
||||
def test_static_file(static_file_directory, file_name):
|
||||
app = Sanic('test_static')
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name))
|
||||
app.static(
|
||||
'/testing2.file', get_file_path(static_file_directory, file_name),
|
||||
name='testing_file')
|
||||
|
||||
uri = app.url_for('static')
|
||||
uri2 = app.url_for('static', filename='any')
|
||||
uri3 = app.url_for('static', name='static', filename='any')
|
||||
|
||||
assert uri == '/testing.file'
|
||||
assert uri == uri2
|
||||
assert uri2 == uri3
|
||||
|
||||
request, response = app.test_client.get(uri)
|
||||
assert response.status == 200
|
||||
assert response.body == get_file_content(static_file_directory, file_name)
|
||||
|
||||
bp = Blueprint('test_bp_static', url_prefix='/bp')
|
||||
|
||||
bp.static('/testing.file', get_file_path(static_file_directory, file_name))
|
||||
bp.static('/testing2.file',
|
||||
get_file_path(static_file_directory, file_name),
|
||||
name='testing_file')
|
||||
|
||||
app.blueprint(bp)
|
||||
|
||||
uri = app.url_for('static', name='test_bp_static.static')
|
||||
uri2 = app.url_for('static', name='test_bp_static.static', filename='any')
|
||||
uri3 = app.url_for('test_bp_static.static')
|
||||
uri4 = app.url_for('test_bp_static.static', name='any')
|
||||
uri5 = app.url_for('test_bp_static.static', filename='any')
|
||||
uri6 = app.url_for('test_bp_static.static', name='any', filename='any')
|
||||
|
||||
assert uri == '/bp/testing.file'
|
||||
assert uri == uri2
|
||||
assert uri2 == uri3
|
||||
assert uri3 == uri4
|
||||
assert uri4 == uri5
|
||||
assert uri5 == uri6
|
||||
|
||||
request, response = app.test_client.get(uri)
|
||||
assert response.status == 200
|
||||
assert response.body == get_file_content(static_file_directory, file_name)
|
||||
|
||||
# test for other parameters
|
||||
uri = app.url_for('static', _external=True, _server='http://localhost')
|
||||
assert uri == 'http://localhost/testing.file'
|
||||
|
||||
uri = app.url_for('static', name='test_bp_static.static',
|
||||
_external=True, _server='http://localhost')
|
||||
assert uri == 'http://localhost/bp/testing.file'
|
||||
|
||||
# test for defined name
|
||||
uri = app.url_for('static', name='testing_file')
|
||||
assert uri == '/testing2.file'
|
||||
|
||||
request, response = app.test_client.get(uri)
|
||||
assert response.status == 200
|
||||
assert response.body == get_file_content(static_file_directory, file_name)
|
||||
|
||||
uri = app.url_for('static', name='test_bp_static.testing_file')
|
||||
assert uri == '/bp/testing2.file'
|
||||
assert uri == app.url_for('static', name='test_bp_static.testing_file',
|
||||
filename='any')
|
||||
|
||||
request, response = app.test_client.get(uri)
|
||||
assert response.status == 200
|
||||
assert response.body == get_file_content(static_file_directory, file_name)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
@pytest.mark.parametrize('base_uri', ['/static', '', '/dir'])
|
||||
def test_static_directory(file_name, base_uri, static_file_directory):
|
||||
|
||||
app = Sanic('test_static')
|
||||
app.static(base_uri, static_file_directory)
|
||||
base_uri2 = base_uri + '/2'
|
||||
app.static(base_uri2, static_file_directory, name='uploads')
|
||||
|
||||
uri = app.url_for('static', name='static', filename=file_name)
|
||||
assert uri == '{}/{}'.format(base_uri, file_name)
|
||||
|
||||
request, response = app.test_client.get(uri)
|
||||
assert response.status == 200
|
||||
assert response.body == get_file_content(static_file_directory, file_name)
|
||||
|
||||
uri2 = app.url_for('static', name='static', filename='/' + file_name)
|
||||
uri3 = app.url_for('static', filename=file_name)
|
||||
uri4 = app.url_for('static', filename='/' + file_name)
|
||||
uri5 = app.url_for('static', name='uploads', filename=file_name)
|
||||
uri6 = app.url_for('static', name='uploads', filename='/' + file_name)
|
||||
|
||||
assert uri == uri2
|
||||
assert uri2 == uri3
|
||||
assert uri3 == uri4
|
||||
|
||||
assert uri5 == '{}/{}'.format(base_uri2, file_name)
|
||||
assert uri5 == uri6
|
||||
|
||||
bp = Blueprint('test_bp_static', url_prefix='/bp')
|
||||
|
||||
bp.static(base_uri, static_file_directory)
|
||||
bp.static(base_uri2, static_file_directory, name='uploads')
|
||||
app.blueprint(bp)
|
||||
|
||||
uri = app.url_for('static', name='test_bp_static.static',
|
||||
filename=file_name)
|
||||
uri2 = app.url_for('static', name='test_bp_static.static',
|
||||
filename='/' + file_name)
|
||||
|
||||
uri4 = app.url_for('static', name='test_bp_static.uploads',
|
||||
filename=file_name)
|
||||
uri5 = app.url_for('static', name='test_bp_static.uploads',
|
||||
filename='/' + file_name)
|
||||
|
||||
assert uri == '/bp{}/{}'.format(base_uri, file_name)
|
||||
assert uri == uri2
|
||||
|
||||
assert uri4 == '/bp{}/{}'.format(base_uri2, file_name)
|
||||
assert uri4 == uri5
|
||||
|
||||
request, response = app.test_client.get(uri)
|
||||
assert response.status == 200
|
||||
assert response.body == get_file_content(static_file_directory, file_name)
|
||||
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_head_request(file_name, static_file_directory):
|
||||
app = Sanic('test_static')
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
|
||||
bp = Blueprint('test_bp_static', url_prefix='/bp')
|
||||
bp.static('/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
app.blueprint(bp)
|
||||
|
||||
uri = app.url_for('static')
|
||||
assert uri == '/testing.file'
|
||||
assert uri == app.url_for('static', name='static')
|
||||
assert uri == app.url_for('static', name='static', filename='any')
|
||||
|
||||
request, response = app.test_client.head(uri)
|
||||
assert response.status == 200
|
||||
assert 'Accept-Ranges' in response.headers
|
||||
assert 'Content-Length' in response.headers
|
||||
assert int(response.headers[
|
||||
'Content-Length']) == len(
|
||||
get_file_content(static_file_directory, file_name))
|
||||
|
||||
# blueprint
|
||||
uri = app.url_for('static', name='test_bp_static.static')
|
||||
assert uri == '/bp/testing.file'
|
||||
assert uri == app.url_for('static', name='test_bp_static.static',
|
||||
filename='any')
|
||||
|
||||
request, response = app.test_client.head(uri)
|
||||
assert response.status == 200
|
||||
assert 'Accept-Ranges' in response.headers
|
||||
assert 'Content-Length' in response.headers
|
||||
assert int(response.headers[
|
||||
'Content-Length']) == len(
|
||||
get_file_content(static_file_directory, file_name))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_content_range_correct(file_name, static_file_directory):
|
||||
app = Sanic('test_static')
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
|
||||
bp = Blueprint('test_bp_static', url_prefix='/bp')
|
||||
bp.static('/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
app.blueprint(bp)
|
||||
|
||||
headers = {
|
||||
'Range': 'bytes=12-19'
|
||||
}
|
||||
uri = app.url_for('static')
|
||||
assert uri == '/testing.file'
|
||||
assert uri == app.url_for('static', name='static')
|
||||
assert uri == app.url_for('static', name='static', filename='any')
|
||||
|
||||
request, response = app.test_client.get(uri, headers=headers)
|
||||
assert response.status == 200
|
||||
assert 'Content-Length' in response.headers
|
||||
assert 'Content-Range' in response.headers
|
||||
static_content = bytes(get_file_content(
|
||||
static_file_directory, file_name))[12:19]
|
||||
assert int(response.headers[
|
||||
'Content-Length']) == len(static_content)
|
||||
assert response.body == static_content
|
||||
|
||||
# blueprint
|
||||
uri = app.url_for('static', name='test_bp_static.static')
|
||||
assert uri == '/bp/testing.file'
|
||||
assert uri == app.url_for('static', name='test_bp_static.static',
|
||||
filename='any')
|
||||
assert uri == app.url_for('test_bp_static.static')
|
||||
assert uri == app.url_for('test_bp_static.static', name='any')
|
||||
assert uri == app.url_for('test_bp_static.static', filename='any')
|
||||
assert uri == app.url_for('test_bp_static.static', name='any',
|
||||
filename='any')
|
||||
|
||||
request, response = app.test_client.get(uri, headers=headers)
|
||||
assert response.status == 200
|
||||
assert 'Content-Length' in response.headers
|
||||
assert 'Content-Range' in response.headers
|
||||
static_content = bytes(get_file_content(
|
||||
static_file_directory, file_name))[12:19]
|
||||
assert int(response.headers[
|
||||
'Content-Length']) == len(static_content)
|
||||
assert response.body == static_content
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_content_range_front(file_name, static_file_directory):
|
||||
app = Sanic('test_static')
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
|
||||
bp = Blueprint('test_bp_static', url_prefix='/bp')
|
||||
bp.static('/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
app.blueprint(bp)
|
||||
|
||||
headers = {
|
||||
'Range': 'bytes=12-'
|
||||
}
|
||||
uri = app.url_for('static')
|
||||
assert uri == '/testing.file'
|
||||
assert uri == app.url_for('static', name='static')
|
||||
assert uri == app.url_for('static', name='static', filename='any')
|
||||
|
||||
request, response = app.test_client.get(uri, headers=headers)
|
||||
assert response.status == 200
|
||||
assert 'Content-Length' in response.headers
|
||||
assert 'Content-Range' in response.headers
|
||||
static_content = bytes(get_file_content(
|
||||
static_file_directory, file_name))[12:]
|
||||
assert int(response.headers[
|
||||
'Content-Length']) == len(static_content)
|
||||
assert response.body == static_content
|
||||
|
||||
# blueprint
|
||||
uri = app.url_for('static', name='test_bp_static.static')
|
||||
assert uri == '/bp/testing.file'
|
||||
assert uri == app.url_for('static', name='test_bp_static.static',
|
||||
filename='any')
|
||||
assert uri == app.url_for('test_bp_static.static')
|
||||
assert uri == app.url_for('test_bp_static.static', name='any')
|
||||
assert uri == app.url_for('test_bp_static.static', filename='any')
|
||||
assert uri == app.url_for('test_bp_static.static', name='any',
|
||||
filename='any')
|
||||
|
||||
request, response = app.test_client.get(uri, headers=headers)
|
||||
assert response.status == 200
|
||||
assert 'Content-Length' in response.headers
|
||||
assert 'Content-Range' in response.headers
|
||||
static_content = bytes(get_file_content(
|
||||
static_file_directory, file_name))[12:]
|
||||
assert int(response.headers[
|
||||
'Content-Length']) == len(static_content)
|
||||
assert response.body == static_content
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_content_range_back(file_name, static_file_directory):
|
||||
app = Sanic('test_static')
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
|
||||
bp = Blueprint('test_bp_static', url_prefix='/bp')
|
||||
bp.static('/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
app.blueprint(bp)
|
||||
|
||||
headers = {
|
||||
'Range': 'bytes=-12'
|
||||
}
|
||||
uri = app.url_for('static')
|
||||
assert uri == '/testing.file'
|
||||
assert uri == app.url_for('static', name='static')
|
||||
assert uri == app.url_for('static', name='static', filename='any')
|
||||
|
||||
request, response = app.test_client.get(uri, headers=headers)
|
||||
assert response.status == 200
|
||||
assert 'Content-Length' in response.headers
|
||||
assert 'Content-Range' in response.headers
|
||||
static_content = bytes(get_file_content(
|
||||
static_file_directory, file_name))[-12:]
|
||||
assert int(response.headers[
|
||||
'Content-Length']) == len(static_content)
|
||||
assert response.body == static_content
|
||||
|
||||
# blueprint
|
||||
uri = app.url_for('static', name='test_bp_static.static')
|
||||
assert uri == '/bp/testing.file'
|
||||
assert uri == app.url_for('static', name='test_bp_static.static',
|
||||
filename='any')
|
||||
assert uri == app.url_for('test_bp_static.static')
|
||||
assert uri == app.url_for('test_bp_static.static', name='any')
|
||||
assert uri == app.url_for('test_bp_static.static', filename='any')
|
||||
assert uri == app.url_for('test_bp_static.static', name='any',
|
||||
filename='any')
|
||||
|
||||
request, response = app.test_client.get(uri, headers=headers)
|
||||
assert response.status == 200
|
||||
assert 'Content-Length' in response.headers
|
||||
assert 'Content-Range' in response.headers
|
||||
static_content = bytes(get_file_content(
|
||||
static_file_directory, file_name))[-12:]
|
||||
assert int(response.headers[
|
||||
'Content-Length']) == len(static_content)
|
||||
assert response.body == static_content
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_content_range_empty(file_name, static_file_directory):
|
||||
app = Sanic('test_static')
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
|
||||
bp = Blueprint('test_bp_static', url_prefix='/bp')
|
||||
bp.static('/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
app.blueprint(bp)
|
||||
|
||||
uri = app.url_for('static')
|
||||
assert uri == '/testing.file'
|
||||
assert uri == app.url_for('static', name='static')
|
||||
assert uri == app.url_for('static', name='static', filename='any')
|
||||
|
||||
request, response = app.test_client.get(uri)
|
||||
assert response.status == 200
|
||||
assert 'Content-Length' in response.headers
|
||||
assert 'Content-Range' not in response.headers
|
||||
assert int(response.headers[
|
||||
'Content-Length']) == len(get_file_content(static_file_directory, file_name))
|
||||
assert response.body == bytes(
|
||||
get_file_content(static_file_directory, file_name))
|
||||
|
||||
# blueprint
|
||||
uri = app.url_for('static', name='test_bp_static.static')
|
||||
assert uri == '/bp/testing.file'
|
||||
assert uri == app.url_for('static', name='test_bp_static.static',
|
||||
filename='any')
|
||||
assert uri == app.url_for('test_bp_static.static')
|
||||
assert uri == app.url_for('test_bp_static.static', name='any')
|
||||
assert uri == app.url_for('test_bp_static.static', filename='any')
|
||||
assert uri == app.url_for('test_bp_static.static', name='any',
|
||||
filename='any')
|
||||
|
||||
request, response = app.test_client.get(uri)
|
||||
assert response.status == 200
|
||||
assert 'Content-Length' in response.headers
|
||||
assert 'Content-Range' not in response.headers
|
||||
assert int(response.headers[
|
||||
'Content-Length']) == len(get_file_content(static_file_directory, file_name))
|
||||
assert response.body == bytes(
|
||||
get_file_content(static_file_directory, file_name))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
|
||||
def test_static_content_range_error(file_name, static_file_directory):
|
||||
app = Sanic('test_static')
|
||||
app.static(
|
||||
'/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
|
||||
bp = Blueprint('test_bp_static', url_prefix='/bp')
|
||||
bp.static('/testing.file', get_file_path(static_file_directory, file_name),
|
||||
use_content_range=True)
|
||||
app.blueprint(bp)
|
||||
|
||||
headers = {
|
||||
'Range': 'bytes=1-0'
|
||||
}
|
||||
uri = app.url_for('static')
|
||||
assert uri == '/testing.file'
|
||||
assert uri == app.url_for('static', name='static')
|
||||
assert uri == app.url_for('static', name='static', filename='any')
|
||||
|
||||
request, response = app.test_client.get(uri, headers=headers)
|
||||
assert response.status == 416
|
||||
assert 'Content-Length' in response.headers
|
||||
assert 'Content-Range' in response.headers
|
||||
assert response.headers['Content-Range'] == "bytes */%s" % (
|
||||
len(get_file_content(static_file_directory, file_name)),)
|
||||
|
||||
# blueprint
|
||||
uri = app.url_for('static', name='test_bp_static.static')
|
||||
assert uri == '/bp/testing.file'
|
||||
assert uri == app.url_for('static', name='test_bp_static.static',
|
||||
filename='any')
|
||||
assert uri == app.url_for('test_bp_static.static')
|
||||
assert uri == app.url_for('test_bp_static.static', name='any')
|
||||
assert uri == app.url_for('test_bp_static.static', filename='any')
|
||||
assert uri == app.url_for('test_bp_static.static', name='any',
|
||||
filename='any')
|
||||
|
||||
request, response = app.test_client.get(uri, headers=headers)
|
||||
assert response.status == 416
|
||||
assert 'Content-Length' in response.headers
|
||||
assert 'Content-Range' in response.headers
|
||||
assert response.headers['Content-Range'] == "bytes */%s" % (
|
||||
len(get_file_content(static_file_directory, file_name)),)
|
|
@ -131,5 +131,5 @@ def test_worker_close(worker):
|
|||
loop.run_until_complete(_close)
|
||||
|
||||
assert worker.signal.stopped == True
|
||||
conn.websocket.close_connection.assert_called_with(force=True)
|
||||
conn.websocket.close_connection.assert_called_with(after_handshake=False)
|
||||
assert len(worker.servers) == 0
|
||||
|
|
5
tox.ini
5
tox.ini
|
@ -10,13 +10,16 @@ deps =
|
|||
coverage
|
||||
pytest
|
||||
pytest-cov
|
||||
pytest-sanic
|
||||
pytest-sugar
|
||||
aiohttp==1.3.5
|
||||
chardet<=2.3.0
|
||||
beautifulsoup4
|
||||
gunicorn
|
||||
commands =
|
||||
pytest tests --cov sanic --cov-report term-missing {posargs}
|
||||
pytest tests --cov sanic --cov-report= {posargs}
|
||||
- coverage combine --append
|
||||
coverage report -m
|
||||
|
||||
[testenv:flake8]
|
||||
deps =
|
||||
|
|
Loading…
Reference in New Issue
Block a user