Merge pull request #5 from channelcat/master
merge upstream master branch
This commit is contained in:
commit
39ea434513
|
@ -31,10 +31,10 @@ 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_vars` 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. You can pass the `load_env` boolean to the Sanic constructor to override that:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
app = Sanic(load_vars=False)
|
app = Sanic(load_env=False)
|
||||||
```
|
```
|
||||||
|
|
||||||
### From an Object
|
### From an Object
|
||||||
|
|
|
@ -1,75 +0,0 @@
|
||||||
# Cookies
|
|
||||||
|
|
||||||
Cookies are pieces of data which persist inside a user's browser. Sanic can
|
|
||||||
both read and write cookies, which are stored as key-value pairs.
|
|
||||||
|
|
||||||
## Reading cookies
|
|
||||||
|
|
||||||
A user's cookies can be accessed via the `Request` object's `cookies` dictionary.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic.response import text
|
|
||||||
|
|
||||||
@app.route("/cookie")
|
|
||||||
async def test(request):
|
|
||||||
test_cookie = request.cookies.get('test')
|
|
||||||
return text("Test cookie set to: {}".format(test_cookie))
|
|
||||||
```
|
|
||||||
|
|
||||||
## Writing cookies
|
|
||||||
|
|
||||||
When returning a response, cookies can be set on the `Response` object.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic.response import text
|
|
||||||
|
|
||||||
@app.route("/cookie")
|
|
||||||
async def test(request):
|
|
||||||
response = text("There's a cookie up in this response")
|
|
||||||
response.cookies['test'] = 'It worked!'
|
|
||||||
response.cookies['test']['domain'] = '.gotta-go-fast.com'
|
|
||||||
response.cookies['test']['httponly'] = True
|
|
||||||
return response
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deleting cookies
|
|
||||||
|
|
||||||
Cookies can be removed semantically or explicitly.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from sanic.response import text
|
|
||||||
|
|
||||||
@app.route("/cookie")
|
|
||||||
async def test(request):
|
|
||||||
response = text("Time to eat some cookies muahaha")
|
|
||||||
|
|
||||||
# This cookie will be set to expire in 0 seconds
|
|
||||||
del response.cookies['kill_me']
|
|
||||||
|
|
||||||
# This cookie will self destruct in 5 seconds
|
|
||||||
response.cookies['short_life'] = 'Glad to be here'
|
|
||||||
response.cookies['short_life']['max-age'] = 5
|
|
||||||
del response.cookies['favorite_color']
|
|
||||||
|
|
||||||
# This cookie will remain unchanged
|
|
||||||
response.cookies['favorite_color'] = 'blue'
|
|
||||||
response.cookies['favorite_color'] = 'pink'
|
|
||||||
del response.cookies['favorite_color']
|
|
||||||
|
|
||||||
return response
|
|
||||||
```
|
|
||||||
|
|
||||||
Response cookies can be set like dictionary values and have the following
|
|
||||||
parameters available:
|
|
||||||
|
|
||||||
- `expires` (datetime): The time for the cookie to expire on the
|
|
||||||
client's browser.
|
|
||||||
- `path` (string): The subset of URLs to which this cookie applies. Defaults to /.
|
|
||||||
- `comment` (string): A comment (metadata).
|
|
||||||
- `domain` (string): Specifies the domain for which the cookie is valid. An
|
|
||||||
explicitly specified domain must always start with a dot.
|
|
||||||
- `max-age` (number): Number of seconds the cookie should live for.
|
|
||||||
- `secure` (boolean): Specifies whether the cookie will only be sent via
|
|
||||||
HTTPS.
|
|
||||||
- `httponly` (boolean): Specifies whether the cookie cannot be read by
|
|
||||||
Javascript.
|
|
87
docs/sanic/cookies.rst
Normal file
87
docs/sanic/cookies.rst
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
Cookies
|
||||||
|
=======
|
||||||
|
|
||||||
|
Cookies are pieces of data which persist inside a user's browser. Sanic can
|
||||||
|
both read and write cookies, which are stored as key-value pairs.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
Cookies can be freely altered by the client. Therefore you cannot just store
|
||||||
|
data such as login information in cookies as-is, as they can be freely altered
|
||||||
|
by the client. To ensure data you store in cookies is not forged or tampered
|
||||||
|
with by the client, use something like `itsdangerous`_ to cryptographically
|
||||||
|
sign the data.
|
||||||
|
|
||||||
|
|
||||||
|
Reading cookies
|
||||||
|
---------------
|
||||||
|
|
||||||
|
A user's cookies can be accessed via the ``Request`` object's ``cookies`` dictionary.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
@app.route("/cookie")
|
||||||
|
async def test(request):
|
||||||
|
test_cookie = request.cookies.get('test')
|
||||||
|
return text("Test cookie set to: {}".format(test_cookie))
|
||||||
|
|
||||||
|
Writing cookies
|
||||||
|
---------------
|
||||||
|
|
||||||
|
When returning a response, cookies can be set on the ``Response`` object.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
@app.route("/cookie")
|
||||||
|
async def test(request):
|
||||||
|
response = text("There's a cookie up in this response")
|
||||||
|
response.cookies['test'] = 'It worked!'
|
||||||
|
response.cookies['test']['domain'] = '.gotta-go-fast.com'
|
||||||
|
response.cookies['test']['httponly'] = True
|
||||||
|
return response
|
||||||
|
|
||||||
|
Deleting cookies
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Cookies can be removed semantically or explicitly.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from sanic.response import text
|
||||||
|
|
||||||
|
@app.route("/cookie")
|
||||||
|
async def test(request):
|
||||||
|
response = text("Time to eat some cookies muahaha")
|
||||||
|
|
||||||
|
# This cookie will be set to expire in 0 seconds
|
||||||
|
del response.cookies['kill_me']
|
||||||
|
|
||||||
|
# This cookie will self destruct in 5 seconds
|
||||||
|
response.cookies['short_life'] = 'Glad to be here'
|
||||||
|
response.cookies['short_life']['max-age'] = 5
|
||||||
|
del response.cookies['favorite_color']
|
||||||
|
|
||||||
|
# This cookie will remain unchanged
|
||||||
|
response.cookies['favorite_color'] = 'blue'
|
||||||
|
response.cookies['favorite_color'] = 'pink'
|
||||||
|
del response.cookies['favorite_color']
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
Response cookies can be set like dictionary values and have the following
|
||||||
|
parameters available:
|
||||||
|
|
||||||
|
- ``expires`` (datetime): The time for the cookie to expire on the client's browser.
|
||||||
|
- ``path`` (string): The subset of URLs to which this cookie applies. Defaults to /.
|
||||||
|
- ``comment`` (string): A comment (metadata).
|
||||||
|
- ``domain`` (string): Specifies the domain for which the cookie is valid. An
|
||||||
|
explicitly specified domain must always start with a dot.
|
||||||
|
- ``max-age`` (number): Number of seconds the cookie should live for.
|
||||||
|
- ``secure`` (boolean): Specifies whether the cookie will only be sent via HTTPS.
|
||||||
|
- ``httponly`` (boolean): Specifies whether the cookie cannot be read by Javascript.
|
||||||
|
|
||||||
|
.. _itsdangerous: https://pythonhosted.org/itsdangerous/
|
|
@ -23,3 +23,4 @@ A list of Sanic extensions created by the community.
|
||||||
- [sanic-prometheus](https://github.com/dkruchinin/sanic-prometheus): Prometheus metrics for Sanic
|
- [sanic-prometheus](https://github.com/dkruchinin/sanic-prometheus): Prometheus metrics for Sanic
|
||||||
- [Sanic-RestPlus](https://github.com/ashleysommer/sanic-restplus): A port of Flask-RestPlus for Sanic. Full-featured REST API with SwaggerUI generation.
|
- [Sanic-RestPlus](https://github.com/ashleysommer/sanic-restplus): A port of Flask-RestPlus for Sanic. Full-featured REST API with SwaggerUI generation.
|
||||||
- [sanic-transmute](https://github.com/yunstanford/sanic-transmute): A Sanic extension that generates APIs from python function and classes, and also generates Swagger UI/documentation automatically.
|
- [sanic-transmute](https://github.com/yunstanford/sanic-transmute): A Sanic extension that generates APIs from python function and classes, and also generates Swagger UI/documentation automatically.
|
||||||
|
- [pytest-sanic](https://github.com/yunstanford/pytest-sanic): A pytest plugin for Sanic. It helps you to test your code asynchronously.
|
||||||
|
|
|
@ -57,3 +57,71 @@ def test_post_json_request_includes_data():
|
||||||
More information about
|
More information about
|
||||||
the available arguments to aiohttp can be found
|
the available arguments to aiohttp can be found
|
||||||
[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session).
|
[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session).
|
||||||
|
|
||||||
|
|
||||||
|
# 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,
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def test_sanic_db_find_by_id(app):
|
||||||
|
"""
|
||||||
|
Let's assume that, in db we have,
|
||||||
|
{
|
||||||
|
"id": "123",
|
||||||
|
"name": "Kobe Bryant",
|
||||||
|
"team": "Lakers",
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
doc = await app.db["players"].find_by_id("123")
|
||||||
|
assert doc.name == "Kobe Bryant"
|
||||||
|
assert doc.team == "Lakers"
|
||||||
|
```
|
||||||
|
|
||||||
|
[pytest-sanic](https://github.com/yunstanford/pytest-sanic) also provides some useful fixtures, like loop, unused_port,
|
||||||
|
test_server, test_client.
|
||||||
|
|
||||||
|
```python
|
||||||
|
@pytest.yield_fixture
|
||||||
|
def app():
|
||||||
|
app = Sanic("test_sanic_app")
|
||||||
|
|
||||||
|
@app.route("/test_get", methods=['GET'])
|
||||||
|
async def test_get(request):
|
||||||
|
return response.json({"GET": True})
|
||||||
|
|
||||||
|
@app.route("/test_post", methods=['POST'])
|
||||||
|
async def test_post(request):
|
||||||
|
return response.json({"POST": True})
|
||||||
|
|
||||||
|
yield app
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def test_cli(loop, app, test_client):
|
||||||
|
return loop.run_until_complete(test_client(app, protocol=WebSocketProtocol))
|
||||||
|
|
||||||
|
|
||||||
|
#########
|
||||||
|
# Tests #
|
||||||
|
#########
|
||||||
|
|
||||||
|
async def test_fixture_test_client_get(test_cli):
|
||||||
|
"""
|
||||||
|
GET request
|
||||||
|
"""
|
||||||
|
resp = await test_cli.get('/test_get')
|
||||||
|
assert resp.status == 200
|
||||||
|
resp_json = await resp.json()
|
||||||
|
assert resp_json == {"GET": True}
|
||||||
|
|
||||||
|
async def test_fixture_test_client_post(test_cli):
|
||||||
|
"""
|
||||||
|
POST request
|
||||||
|
"""
|
||||||
|
resp = await test_cli.post('/test_post')
|
||||||
|
assert resp.status == 200
|
||||||
|
resp_json = await resp.json()
|
||||||
|
assert resp_json == {"POST": True}
|
||||||
|
```
|
||||||
|
|
|
@ -202,4 +202,10 @@ class Config(dict):
|
||||||
for k, v in os.environ.items():
|
for k, v in os.environ.items():
|
||||||
if k.startswith(SANIC_PREFIX):
|
if k.startswith(SANIC_PREFIX):
|
||||||
_, config_key = k.split(SANIC_PREFIX, 1)
|
_, config_key = k.split(SANIC_PREFIX, 1)
|
||||||
|
try:
|
||||||
|
self[config_key] = int(v)
|
||||||
|
except ValueError:
|
||||||
|
try:
|
||||||
|
self[config_key] = float(v)
|
||||||
|
except ValueError:
|
||||||
self[config_key] = v
|
self[config_key] = v
|
||||||
|
|
|
@ -194,6 +194,11 @@ class ContentRangeError(SanicException):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@add_status_code(403)
|
||||||
|
class Forbidden(SanicException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class InvalidRangeType(ContentRangeError):
|
class InvalidRangeType(ContentRangeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -86,13 +86,13 @@ class Request(dict):
|
||||||
|
|
||||||
:return: token related to request
|
:return: token related to request
|
||||||
"""
|
"""
|
||||||
prefixes = ('Token ', 'Bearer ')
|
prefixes = ('Bearer', 'Token')
|
||||||
auth_header = self.headers.get('Authorization')
|
auth_header = self.headers.get('Authorization')
|
||||||
|
|
||||||
if auth_header is not None:
|
if auth_header is not None:
|
||||||
for prefix in prefixes:
|
for prefix in prefixes:
|
||||||
if prefix in auth_header:
|
if prefix in auth_header:
|
||||||
return auth_header.partition(prefix)[-1]
|
return auth_header.partition(prefix)[-1].strip()
|
||||||
|
|
||||||
return auth_header
|
return auth_header
|
||||||
|
|
||||||
|
|
|
@ -75,7 +75,7 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
signal=Signal(), connections=set(), request_timeout=60,
|
signal=Signal(), connections=set(), request_timeout=60,
|
||||||
request_max_size=None, request_class=None, has_log=True,
|
request_max_size=None, request_class=None, has_log=True,
|
||||||
keep_alive=True, is_request_stream=False, router=None,
|
keep_alive=True, is_request_stream=False, router=None,
|
||||||
state=None, **kwargs):
|
state=None, debug=False, **kwargs):
|
||||||
self.loop = loop
|
self.loop = loop
|
||||||
self.transport = None
|
self.transport = None
|
||||||
self.request = None
|
self.request = None
|
||||||
|
@ -99,15 +99,18 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
self._request_handler_task = None
|
self._request_handler_task = None
|
||||||
self._request_stream_task = None
|
self._request_stream_task = None
|
||||||
self._keep_alive = keep_alive
|
self._keep_alive = keep_alive
|
||||||
|
self._header_fragment = b''
|
||||||
self.state = state if state else {}
|
self.state = state if state else {}
|
||||||
if 'requests_count' not in self.state:
|
if 'requests_count' not in self.state:
|
||||||
self.state['requests_count'] = 0
|
self.state['requests_count'] = 0
|
||||||
|
self._debug = debug
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def keep_alive(self):
|
def keep_alive(self):
|
||||||
return (self._keep_alive
|
return (
|
||||||
and not self.signal.stopped
|
self._keep_alive and
|
||||||
and self.parser.should_keep_alive())
|
not self.signal.stopped and
|
||||||
|
self.parser.should_keep_alive())
|
||||||
|
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
# Connection
|
# Connection
|
||||||
|
@ -164,18 +167,32 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
try:
|
try:
|
||||||
self.parser.feed_data(data)
|
self.parser.feed_data(data)
|
||||||
except HttpParserError:
|
except HttpParserError:
|
||||||
exception = InvalidUsage('Bad Request')
|
message = 'Bad Request'
|
||||||
|
if self._debug:
|
||||||
|
message += '\n' + traceback.format_exc()
|
||||||
|
exception = InvalidUsage(message)
|
||||||
self.write_error(exception)
|
self.write_error(exception)
|
||||||
|
|
||||||
def on_url(self, url):
|
def on_url(self, url):
|
||||||
|
if not self.url:
|
||||||
self.url = url
|
self.url = url
|
||||||
|
else:
|
||||||
|
self.url += url
|
||||||
|
|
||||||
def on_header(self, name, value):
|
def on_header(self, name, value):
|
||||||
if name == b'Content-Length' and int(value) > self.request_max_size:
|
self._header_fragment += name
|
||||||
|
|
||||||
|
if value is not None:
|
||||||
|
if self._header_fragment == b'Content-Length' \
|
||||||
|
and int(value) > self.request_max_size:
|
||||||
exception = PayloadTooLarge('Payload Too Large')
|
exception = PayloadTooLarge('Payload Too Large')
|
||||||
self.write_error(exception)
|
self.write_error(exception)
|
||||||
|
|
||||||
self.headers.append((name.decode().casefold(), value.decode()))
|
self.headers.append(
|
||||||
|
(self._header_fragment.decode().casefold(),
|
||||||
|
value.decode()))
|
||||||
|
|
||||||
|
self._header_fragment = b''
|
||||||
|
|
||||||
def on_headers_complete(self):
|
def on_headers_complete(self):
|
||||||
self.request = self.request_class(
|
self.request = self.request_class(
|
||||||
|
@ -459,7 +476,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
router=router,
|
router=router,
|
||||||
websocket_max_size=websocket_max_size,
|
websocket_max_size=websocket_max_size,
|
||||||
websocket_max_queue=websocket_max_queue,
|
websocket_max_queue=websocket_max_queue,
|
||||||
state=state
|
state=state,
|
||||||
|
debug=debug,
|
||||||
)
|
)
|
||||||
|
|
||||||
server_coroutine = loop.create_server(
|
server_coroutine = loop.create_server(
|
||||||
|
|
|
@ -4,7 +4,7 @@ from bs4 import BeautifulSoup
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
from sanic.exceptions import InvalidUsage, ServerError, NotFound, Unauthorized
|
from sanic.exceptions import InvalidUsage, ServerError, NotFound, Unauthorized
|
||||||
from sanic.exceptions import abort
|
from sanic.exceptions import Forbidden, abort
|
||||||
|
|
||||||
|
|
||||||
class SanicExceptionTestException(Exception):
|
class SanicExceptionTestException(Exception):
|
||||||
|
@ -27,6 +27,10 @@ def exception_app():
|
||||||
def handler_404(request):
|
def handler_404(request):
|
||||||
raise NotFound("OK")
|
raise NotFound("OK")
|
||||||
|
|
||||||
|
@app.route('/403')
|
||||||
|
def handler_403(request):
|
||||||
|
raise Forbidden("Forbidden")
|
||||||
|
|
||||||
@app.route('/401/basic')
|
@app.route('/401/basic')
|
||||||
def handler_401_basic(request):
|
def handler_401_basic(request):
|
||||||
raise Unauthorized("Unauthorized", "Basic", "Sanic")
|
raise Unauthorized("Unauthorized", "Basic", "Sanic")
|
||||||
|
@ -108,6 +112,12 @@ def test_not_found_exception(exception_app):
|
||||||
assert response.status == 404
|
assert response.status == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_forbidden_exception(exception_app):
|
||||||
|
"""Test the built-in Forbidden exception"""
|
||||||
|
request, response = exception_app.test_client.get('/403')
|
||||||
|
assert response.status == 403
|
||||||
|
|
||||||
|
|
||||||
def test_unauthorized_exception(exception_app):
|
def test_unauthorized_exception(exception_app):
|
||||||
"""Test the built-in Unauthorized exception"""
|
"""Test the built-in Unauthorized exception"""
|
||||||
request, response = exception_app.test_client.get('/401/basic')
|
request, response = exception_app.test_client.get('/401/basic')
|
||||||
|
|
|
@ -172,16 +172,6 @@ def test_token():
|
||||||
|
|
||||||
assert request.token == token
|
assert request.token == token
|
||||||
|
|
||||||
token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf'
|
|
||||||
headers = {
|
|
||||||
'content-type': 'application/json',
|
|
||||||
'Authorization': 'Bearer Token {}'.format(token)
|
|
||||||
}
|
|
||||||
|
|
||||||
request, response = app.test_client.get('/', headers=headers)
|
|
||||||
|
|
||||||
assert request.token == token
|
|
||||||
|
|
||||||
token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf'
|
token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf'
|
||||||
headers = {
|
headers = {
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
|
|
Loading…
Reference in New Issue
Block a user