Merge pull request #98 from channelcat/cookies
Adding cookie capabilities for issue #74
This commit is contained in:
commit
658ced9188
|
@ -50,6 +50,7 @@ app.run(host="0.0.0.0", port=8000)
|
||||||
* [Middleware](docs/middleware.md)
|
* [Middleware](docs/middleware.md)
|
||||||
* [Exceptions](docs/exceptions.md)
|
* [Exceptions](docs/exceptions.md)
|
||||||
* [Blueprints](docs/blueprints.md)
|
* [Blueprints](docs/blueprints.md)
|
||||||
|
* [Cookies](docs/cookies.md)
|
||||||
* [Deploying](docs/deploying.md)
|
* [Deploying](docs/deploying.md)
|
||||||
* [Contributing](docs/contributing.md)
|
* [Contributing](docs/contributing.md)
|
||||||
* [License](LICENSE)
|
* [License](LICENSE)
|
||||||
|
|
50
docs/cookies.md
Normal file
50
docs/cookies.md
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
# Cookies
|
||||||
|
|
||||||
|
## Request
|
||||||
|
|
||||||
|
Request cookies can be accessed via the request.cookie dictionary
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
from sanic import Sanic
|
||||||
|
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))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Response
|
||||||
|
|
||||||
|
Response cookies can be set like dictionary values and
|
||||||
|
have the following parameters available:
|
||||||
|
|
||||||
|
* expires - datetime - Time for cookie to expire on the client's browser
|
||||||
|
* path - string - The Path attribute specifies the subset of URLs to
|
||||||
|
which this cookie applies
|
||||||
|
* comment - string - Cookie 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
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
from sanic import Sanic
|
||||||
|
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
|
||||||
|
```
|
|
@ -1,5 +1,6 @@
|
||||||
from cgi import parse_header
|
from cgi import parse_header
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
from http.cookies import SimpleCookie
|
||||||
from httptools import parse_url
|
from httptools import parse_url
|
||||||
from urllib.parse import parse_qs
|
from urllib.parse import parse_qs
|
||||||
from ujson import loads as json_loads
|
from ujson import loads as json_loads
|
||||||
|
@ -30,7 +31,7 @@ class Request:
|
||||||
Properties of an HTTP request such as URL, headers, etc.
|
Properties of an HTTP request such as URL, headers, etc.
|
||||||
"""
|
"""
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
'url', 'headers', 'version', 'method',
|
'url', 'headers', 'version', 'method', '_cookies',
|
||||||
'query_string', 'body',
|
'query_string', 'body',
|
||||||
'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
|
'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
|
||||||
)
|
)
|
||||||
|
@ -52,6 +53,7 @@ class Request:
|
||||||
self.parsed_form = None
|
self.parsed_form = None
|
||||||
self.parsed_files = None
|
self.parsed_files = None
|
||||||
self.parsed_args = None
|
self.parsed_args = None
|
||||||
|
self._cookies = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def json(self):
|
def json(self):
|
||||||
|
@ -105,6 +107,18 @@ class Request:
|
||||||
|
|
||||||
return self.parsed_args
|
return self.parsed_args
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cookies(self):
|
||||||
|
if self._cookies is None:
|
||||||
|
if 'Cookie' in self.headers:
|
||||||
|
cookies = SimpleCookie()
|
||||||
|
cookies.load(self.headers['Cookie'])
|
||||||
|
self._cookies = {name: cookie.value
|
||||||
|
for name, cookie in cookies.items()}
|
||||||
|
else:
|
||||||
|
self._cookies = {}
|
||||||
|
return self._cookies
|
||||||
|
|
||||||
|
|
||||||
File = namedtuple('File', ['type', 'body', 'name'])
|
File = namedtuple('File', ['type', 'body', 'name'])
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from datetime import datetime
|
||||||
|
from http.cookies import SimpleCookie
|
||||||
import ujson
|
import ujson
|
||||||
|
|
||||||
COMMON_STATUS_CODES = {
|
COMMON_STATUS_CODES = {
|
||||||
|
@ -68,7 +70,7 @@ ALL_STATUS_CODES = {
|
||||||
|
|
||||||
|
|
||||||
class HTTPResponse:
|
class HTTPResponse:
|
||||||
__slots__ = ('body', 'status', 'content_type', 'headers')
|
__slots__ = ('body', 'status', 'content_type', 'headers', '_cookies')
|
||||||
|
|
||||||
def __init__(self, body=None, status=200, headers=None,
|
def __init__(self, body=None, status=200, headers=None,
|
||||||
content_type='text/plain', body_bytes=b''):
|
content_type='text/plain', body_bytes=b''):
|
||||||
|
@ -81,6 +83,7 @@ class HTTPResponse:
|
||||||
|
|
||||||
self.status = status
|
self.status = status
|
||||||
self.headers = headers or {}
|
self.headers = headers or {}
|
||||||
|
self._cookies = None
|
||||||
|
|
||||||
def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None):
|
def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None):
|
||||||
# This is all returned in a kind-of funky way
|
# This is all returned in a kind-of funky way
|
||||||
|
@ -95,6 +98,12 @@ class HTTPResponse:
|
||||||
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8'))
|
b'%b: %b\r\n' % (name.encode(), value.encode('utf-8'))
|
||||||
for name, value in self.headers.items()
|
for name, value in self.headers.items()
|
||||||
)
|
)
|
||||||
|
if self._cookies:
|
||||||
|
for cookie in self._cookies.values():
|
||||||
|
if type(cookie['expires']) is datetime:
|
||||||
|
cookie['expires'] = \
|
||||||
|
cookie['expires'].strftime("%a, %d-%b-%Y %T GMT")
|
||||||
|
headers += (str(self._cookies) + "\r\n").encode('utf-8')
|
||||||
|
|
||||||
# Try to pull from the common codes first
|
# Try to pull from the common codes first
|
||||||
# Speeds up response rate 6% over pulling from all
|
# Speeds up response rate 6% over pulling from all
|
||||||
|
@ -119,6 +128,12 @@ class HTTPResponse:
|
||||||
self.body
|
self.body
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cookies(self):
|
||||||
|
if self._cookies is None:
|
||||||
|
self._cookies = SimpleCookie()
|
||||||
|
return self._cookies
|
||||||
|
|
||||||
|
|
||||||
def json(body, status=200, headers=None):
|
def json(body, status=200, headers=None):
|
||||||
return HTTPResponse(ujson.dumps(body), headers=headers, status=status,
|
return HTTPResponse(ujson.dumps(body), headers=headers, status=status,
|
||||||
|
|
|
@ -5,10 +5,10 @@ HOST = '127.0.0.1'
|
||||||
PORT = 42101
|
PORT = 42101
|
||||||
|
|
||||||
|
|
||||||
async def local_request(method, uri, *args, **kwargs):
|
async def local_request(method, uri, cookies=None, *args, **kwargs):
|
||||||
url = 'http://{host}:{port}{uri}'.format(host=HOST, port=PORT, uri=uri)
|
url = 'http://{host}:{port}{uri}'.format(host=HOST, port=PORT, uri=uri)
|
||||||
log.info(url)
|
log.info(url)
|
||||||
async with aiohttp.ClientSession() as session:
|
async with aiohttp.ClientSession(cookies=cookies) as session:
|
||||||
async with getattr(session, method)(url, *args, **kwargs) as response:
|
async with getattr(session, method)(url, *args, **kwargs) as response:
|
||||||
response.text = await response.text()
|
response.text = await response.text()
|
||||||
return response
|
return response
|
||||||
|
|
44
tests/test_cookies.py
Normal file
44
tests/test_cookies.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from http.cookies import SimpleCookie
|
||||||
|
from sanic import Sanic
|
||||||
|
from sanic.response import json, text
|
||||||
|
from sanic.utils import sanic_endpoint_test
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ #
|
||||||
|
# GET
|
||||||
|
# ------------------------------------------------------------ #
|
||||||
|
|
||||||
|
def test_cookies():
|
||||||
|
app = Sanic('test_text')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def handler(request):
|
||||||
|
response = text('Cookies are: {}'.format(request.cookies['test']))
|
||||||
|
response.cookies['right_back'] = 'at you'
|
||||||
|
return response
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app, cookies={"test": "working!"})
|
||||||
|
response_cookies = SimpleCookie()
|
||||||
|
response_cookies.load(response.headers.get('Set-Cookie', {}))
|
||||||
|
|
||||||
|
assert response.text == 'Cookies are: working!'
|
||||||
|
assert response_cookies['right_back'].value == 'at you'
|
||||||
|
|
||||||
|
def test_cookie_options():
|
||||||
|
app = Sanic('test_text')
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def handler(request):
|
||||||
|
response = text("OK")
|
||||||
|
response.cookies['test'] = 'at you'
|
||||||
|
response.cookies['test']['httponly'] = True
|
||||||
|
response.cookies['test']['expires'] = datetime.now() + timedelta(seconds=10)
|
||||||
|
return response
|
||||||
|
|
||||||
|
request, response = sanic_endpoint_test(app)
|
||||||
|
response_cookies = SimpleCookie()
|
||||||
|
response_cookies.load(response.headers.get('Set-Cookie', {}))
|
||||||
|
|
||||||
|
assert response_cookies['test'].value == 'at you'
|
||||||
|
assert response_cookies['test']['httponly'] == True
|
Loading…
Reference in New Issue
Block a user