Merge pull request #5 from channelcat/master

merge upstream master branch
This commit is contained in:
7 2017-07-08 14:23:00 -07:00 committed by GitHub
commit 39ea434513
11 changed files with 212 additions and 102 deletions

View File

@ -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

View File

@ -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
View 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/

View File

@ -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.

View File

@ -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}
```

View File

@ -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

View File

@ -194,6 +194,11 @@ class ContentRangeError(SanicException):
} }
@add_status_code(403)
class Forbidden(SanicException):
pass
class InvalidRangeType(ContentRangeError): class InvalidRangeType(ContentRangeError):
pass pass

View File

@ -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

View File

@ -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(

View File

@ -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')

View File

@ -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',