sanic/sanic/response.py
2017-01-26 18:37:16 -08:00

275 lines
8.7 KiB
Python

from aiofiles import open as open_async
from mimetypes import guess_type
from os import path
from ujson import dumps as json_dumps
from .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 = {
100: b'Continue',
101: b'Switching Protocols',
102: b'Processing',
200: b'OK',
201: b'Created',
202: b'Accepted',
203: b'Non-Authoritative Information',
204: b'No Content',
205: b'Reset Content',
206: b'Partial Content',
207: b'Multi-Status',
208: b'Already Reported',
226: b'IM Used',
300: b'Multiple Choices',
301: b'Moved Permanently',
302: b'Found',
303: b'See Other',
304: b'Not Modified',
305: b'Use Proxy',
307: b'Temporary Redirect',
308: b'Permanent Redirect',
400: b'Bad Request',
401: b'Unauthorized',
402: b'Payment Required',
403: b'Forbidden',
404: b'Not Found',
405: b'Method Not Allowed',
406: b'Not Acceptable',
407: b'Proxy Authentication Required',
408: b'Request Timeout',
409: b'Conflict',
410: b'Gone',
411: b'Length Required',
412: b'Precondition Failed',
413: b'Request Entity Too Large',
414: b'Request-URI Too Long',
415: b'Unsupported Media Type',
416: b'Requested Range Not Satisfiable',
417: b'Expectation Failed',
422: b'Unprocessable Entity',
423: b'Locked',
424: b'Failed Dependency',
426: b'Upgrade Required',
428: b'Precondition Required',
429: b'Too Many Requests',
431: b'Request Header Fields Too Large',
500: b'Internal Server Error',
501: b'Not Implemented',
502: b'Bad Gateway',
503: b'Service Unavailable',
504: b'Gateway Timeout',
505: b'HTTP Version Not Supported',
506: b'Variant Also Negotiates',
507: b'Insufficient Storage',
508: b'Loop Detected',
510: b'Not Extended',
511: b'Network Authentication Required'
}
class HTTPResponse:
__slots__ = ('body', 'status', 'content_type', 'headers', '_cookies')
def __init__(self, body=None, status=200, headers=None,
content_type='text/plain', body_bytes=b''):
self.content_type = content_type
if body is not None:
try:
# Try to encode it regularly
self.body = body.encode()
except AttributeError:
# Convert it to a str if you can't
self.body = str(body).encode()
else:
self.body = body_bytes
self.status = status
self.headers = headers or {}
self._cookies = None
def output(self, version="1.1", keep_alive=False, keep_alive_timeout=None):
# This is all returned in a kind-of funky way
# We tried to make this as fast as possible in pure python
if keep_alive and keep_alive_timeout:
if 'Keep-Alive' not in self.headers:
self.headers['Keep-Alive'] = keep_alive_timeout
if 'Connection' not in self.headers:
if keep_alive:
self.headers['Connection'] = 'keep-alive'
else:
self.headers['Connection'] = 'close'
if 'Content-Length' not in self.headers:
self.headers['Content-Length'] = len(self.body)
if 'Content-Type' not in self.headers:
self.headers['Content-Type'] = self.content_type
headers = b''
if self.headers:
for name, value in self.headers.items():
try:
headers += (b'%b: %b\r\n' % (
name.encode(), value.encode('utf-8')))
except AttributeError:
headers += (b'%b: %b\r\n' % (
str(name).encode(), str(value).encode('utf-8')))
# Try to pull from the common codes first
# 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)
return (b'HTTP/%b %d %b\r\n'
b'%b\r\n'
b'%b') % (
version.encode(),
self.status,
status,
headers,
self.body
)
@property
def cookies(self):
if self._cookies is None:
self._cookies = CookieJar(self.headers)
return self._cookies
class ContentRangeHandler:
"""
This class is for parsing the request header
"""
__slots__ = ('start', 'end', 'size', 'total', 'headers')
def __init__(self, request, stats):
self.start = self.size = 0
self.end = None
self.headers = dict()
self.total = stats.st_size
range_header = request.headers.get('Range')
if range_header:
self.start, self.end = ContentRangeHandler.parse_range(range_header)
if self.start is not None and self.end is not None:
self.size = self.end - self.start
elif self.end is not None:
self.size = self.end
elif self.start is not None:
self.size = self.total - self.start
else:
self.size = self.total
self.headers['Content-Range'] = "bytes %s-%s/%s" % (
self.start, self.end, self.total)
else:
self.size = self.total
def __bool__(self):
return self.size > 0
@staticmethod
def parse_range(range_header):
unit, _, value = tuple(map(str.strip, range_header.partition('=')))
if unit != 'bytes':
return None
start_b, _, end_b = tuple(map(str.strip, value.partition('-')))
try:
start = int(start_b) if start_b.strip() else None
end = int(end_b) if end_b.strip() else None
except ValueError:
return None
if end is not None:
if start is None:
if end != 0:
start = -end
end = None
return start, end
def json(body, status=200, headers=None, **kwargs):
"""
Returns response object with body in json format.
:param body: Response data to be serialized.
:param status: Response code.
:param headers: Custom Headers.
:param kwargs: Remaining arguments that are passed to the json encoder.
"""
return HTTPResponse(json_dumps(body, **kwargs), headers=headers,
status=status, content_type="application/json")
def text(body, status=200, headers=None):
"""
Returns response object with body in text format.
:param body: Response data to be encoded.
:param status: Response code.
:param headers: Custom Headers.
"""
return HTTPResponse(body, status=status, headers=headers,
content_type="text/plain; charset=utf-8")
def html(body, status=200, headers=None):
"""
Returns response object with body in html format.
:param body: Response data to be encoded.
:param status: Response code.
:param headers: Custom Headers.
"""
return HTTPResponse(body, status=status, headers=headers,
content_type="text/html; charset=utf-8")
async def file(location, mime_type=None, headers=None, _range=None):
"""
Returns response object with file data.
:param location: Location of file on system.
:param mime_type: Specific mime_type.
:param headers: Custom Headers.
:param _range:
"""
filename = path.split(location)[-1]
async with open_async(location, mode='rb') as _file:
if _range:
await _file.seek(_range.start)
out_stream = await _file.read(_range.size)
headers['Content-Range'] = 'bytes %s-%s/%s' % (
_range.start, _range.end, _range.total)
else:
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)
def redirect(to, headers=None, status=302,
content_type="text/html; charset=utf-8"):
"""
Aborts execution and causes a 302 redirect (by default).
:param to: path or fully qualified URL to redirect to
:param headers: optional dict of headers to include in the new request
:param status: status code (int) of the new request, defaults to 302
:param content_type:
the content type (string) of the response
:returns: the redirecting Response
"""
headers = headers or {}
# According to RFC 7231, a relative URI is now permitted.
headers['Location'] = to
return HTTPResponse(
status=status,
headers=headers,
content_type=content_type)