ranchimallflo-api/py3.7/lib/python3.7/site-packages/quart/wrappers/response.py

617 lines
21 KiB
Python

from abc import ABC, abstractmethod
from datetime import datetime, timedelta
from email.utils import parsedate_to_datetime
from hashlib import md5
from inspect import isasyncgen
from os import PathLike
from types import TracebackType
from typing import (
AnyStr, AsyncGenerator, AsyncIterable, AsyncIterator, Iterable, Optional, Tuple,
TYPE_CHECKING, Union,
)
from wsgiref.handlers import format_date_time
from aiofiles import open as async_open
from aiofiles.base import AiofilesContextManager
from aiofiles.threadpool import AsyncFileIO
from ._base import _BaseRequestResponse, JSONMixin
from ..datastructures import (
CIMultiDict, ContentRange, Headers, HeaderSet, Range, ResponseAccessControl,
ResponseCacheControl,
)
from ..utils import create_cookie, file_path_to_path
if TYPE_CHECKING:
from ..routing import Rule # noqa
class ResponseBody(ABC):
"""Base class wrapper for response body data.
This ensures that the following is possible (as Quart assumes so
when returning the body to the ASGI server
async with wrapper as response:
async for data in response:
send(data)
"""
@abstractmethod
async def __aenter__(self) -> AsyncIterable:
pass
@abstractmethod
async def __aexit__(self, exc_type: type, exc_value: BaseException, tb: TracebackType) -> None:
pass
@abstractmethod
async def convert_to_sequence(self) -> bytes:
pass
def _raise_if_invalid_range(begin: int, end: int, size: int) -> None:
if begin >= end or abs(begin) > size or end > size:
from ..exceptions import RequestRangeNotSatisfiable
raise RequestRangeNotSatisfiable()
class DataBody(ResponseBody):
def __init__(self, data: bytes) -> None:
self.data = data
self.begin = 0
self.end = len(self.data)
async def __aenter__(self) -> "DataBody":
return self
async def __aexit__(self, exc_type: type, exc_value: BaseException, tb: TracebackType) -> None:
pass
def __aiter__(self) -> AsyncIterator:
async def _aiter() -> AsyncGenerator[bytes, None]:
yield self.data[self.begin:self.end]
return _aiter()
async def convert_to_sequence(self) -> bytes:
return self.data[self.begin:self.end]
async def make_conditional(
self, begin: int, end: Optional[int], max_partial_size: Optional[int]=None,
) -> int:
self.begin = begin
self.end = len(self.data) if end is None else end
if max_partial_size is not None:
self.end = min(self.begin + max_partial_size, self.end)
_raise_if_invalid_range(self.begin, self.end, len(self.data))
return len(self.data)
class IterableBody(ResponseBody):
def __init__(self, iterable: Union[AsyncGenerator[bytes, None], Iterable]) -> None:
self.iter: AsyncGenerator[bytes, None]
if isasyncgen(iterable):
self.iter = iterable # type: ignore
else:
async def _aiter() -> AsyncGenerator[bytes, None]:
for data in iterable: # type: ignore
yield data
self.iter = _aiter()
async def __aenter__(self) -> "IterableBody":
return self
async def __aexit__(self, exc_type: type, exc_value: BaseException, tb: TracebackType) -> None:
pass
def __aiter__(self) -> AsyncIterator:
return self.iter
async def convert_to_sequence(self) -> bytes:
result = bytearray()
async for data in self.iter:
result.extend(data)
return bytes(result)
class FileBody(ResponseBody):
"""Provides an async file accessor with range setting.
The :attr:`Response.response` attribute must be async-iterable and
yield bytes, which this wrapper does for a file. In addition it
allows a range to be set on the file, thereby supporting
conditional requests.
Attributes:
buffer_size: Size in bytes to load per iteration.
"""
buffer_size = 8192
def __init__(
self, file_path: Union[str, PathLike], *, buffer_size: Optional[int] = None,
) -> None:
self.file_path = file_path_to_path(file_path)
self.size = self.file_path.stat().st_size
self.begin = 0
self.end = self.size
if buffer_size is not None:
self.buffer_size = buffer_size
self.file: Optional[AsyncFileIO] = None
self.file_manager: Optional[AiofilesContextManager] = None
async def __aenter__(self) -> "FileBody":
self.file_manager = async_open(self.file_path, mode="rb")
self.file = await self.file_manager.__aenter__()
await self.file.seek(self.begin)
return self
async def __aexit__(self, exc_type: type, exc_value: BaseException, tb: TracebackType) -> None:
await self.file_manager.__aexit__(exc_type, exc_value, tb)
def __aiter__(self) -> 'FileBody':
return self
async def __anext__(self) -> bytes:
current = await self.file.tell()
if current >= self.end:
raise StopAsyncIteration()
read_size = min(self.buffer_size, self.end - current)
chunk = await self.file.read(read_size)
if chunk:
return chunk
else:
raise StopAsyncIteration()
async def convert_to_sequence(self) -> bytes:
result = bytearray()
async with self as response:
async for data in response:
result.extend(data)
return bytes(result)
async def make_conditional(
self, begin: int, end: Optional[int], max_partial_size: Optional[int]=None,
) -> int:
self.begin = begin
self.end = self.size if end is None else end
if max_partial_size is not None:
self.end = min(self.begin + max_partial_size, self.end)
_raise_if_invalid_range(self.begin, self.end, self.size)
return self.size
class Response(_BaseRequestResponse, JSONMixin):
"""This class represents a response.
It can be subclassed and the subclassed used in preference by
replacing the :attr:`~quart.Quart.response_class` with your
subclass.
Attributes:
automatically_set_content_length: If False the content length
header must be provided.
default_status: The status code to use if not provided.
default_mimetype: The mimetype to use if not provided.
implicit_sequence_conversion: Implicitly convert the response
to a iterable in the get_data method, to allow multiple
iterations.
"""
automatically_set_content_length = True
default_status = 200
default_mimetype = 'text/html'
data_body_class = DataBody
file_body_class = FileBody
implicit_sequence_conversion = True
iterable_body_class = IterableBody
def __init__(
self,
response: Union[ResponseBody, AnyStr, Iterable],
status: Optional[int]=None,
headers: Optional[Union[dict, CIMultiDict, Headers]]=None,
mimetype: Optional[str]=None,
content_type: Optional[str]=None,
*,
timeout: Optional[int]=None,
) -> None:
"""Create a response object.
The response itself can be a chunk of data or a
iterable/generator of data chunks.
The Content-Type can either be specified as a mimetype or
content_type header or omitted to use the
:attr:`default_mimetype`.
Arguments:
response: The response data or iterable over the data.
status_code: Status code of the response.
headers: Headers to attach to the response.
mimetype: Mimetype of the response.
content_type: Content-Type header value.
timeout: Optional argument to specify timeout when sending
response data.
Attributes:
response: An iterable of the response bytes-data.
"""
super().__init__(headers)
self.timeout = timeout
if status is None:
status = self.default_status
try:
self.status_code = int(status)
except ValueError as error:
raise ValueError('Quart does not support non-integer status values') from error
if content_type is None:
if mimetype is None and 'content-type' not in self.headers:
mimetype = self.default_mimetype
if mimetype is not None:
self.mimetype = mimetype
if content_type is not None:
self.headers['Content-Type'] = content_type
self.response: ResponseBody
if isinstance(response, ResponseBody):
self.response = response
elif isinstance(response, (str, bytes)):
self.set_data(response) # type: ignore
else:
self.response = self.iterable_body_class(response)
async def get_data(self, raw: bool=True) -> AnyStr:
"""Return the body data."""
if self.implicit_sequence_conversion:
self.response = self.data_body_class(await self.response.convert_to_sequence())
result = b'' if raw else ''
async with self.response as body: # type: ignore
async for data in body:
if raw:
result += data
else:
result += data.decode(self.charset)
return result # type: ignore
def set_data(self, data: AnyStr) -> None:
"""Set the response data.
This will encode using the :attr:`charset`.
"""
if isinstance(data, str):
bytes_data = data.encode(self.charset)
else:
bytes_data = data
self.response = self.data_body_class(bytes_data)
if self.automatically_set_content_length:
self.content_length = len(bytes_data)
async def make_conditional(
self,
request_range: Range,
max_partial_size: Optional[int]=None,
) -> None:
"""Make the response conditional to the
Arguments:
request_range: The range as requested by the request.
max_partial_size: The maximum length the server is willing
to serve in a single response. Defaults to unlimited.
"""
self.accept_ranges = "bytes" # Advertise this ability
if len(request_range.ranges) == 0: # Not a conditional request
return
if request_range.units != "bytes" or len(request_range.ranges) > 1:
from ..exceptions import RequestRangeNotSatisfiable
raise RequestRangeNotSatisfiable()
begin, end = request_range.ranges[0]
try:
complete_length = await self.response.make_conditional( # type: ignore
begin, end, max_partial_size,
)
except AttributeError:
self.response = self.data_body_class(await self.response.convert_to_sequence())
return await self.make_conditional(request_range, max_partial_size)
else:
self.content_length = self.response.end - self.response.begin # type: ignore
if self.content_length != complete_length:
self.content_range = ContentRange(
request_range.units,
self.response.begin, self.response.end - 1, # type: ignore
complete_length,
)
self.status_code = 206
async def freeze(self) -> None:
"""Freeze this object ready for pickling."""
self.set_data((await self.get_data()))
def set_cookie( # type: ignore
self,
key: str,
value: AnyStr='',
max_age: Optional[Union[int, timedelta]]=None,
expires: Optional[datetime]=None,
path: str='/',
domain: Optional[str]=None,
secure: bool=False,
httponly: bool=False,
) -> None:
"""Set a cookie in the response headers.
The arguments are the standard cookie morsels and this is a
wrapper around the stdlib SimpleCookie code.
"""
if isinstance(value, bytes):
value = value.decode() # type: ignore
cookie = create_cookie(key, value, max_age, expires, path, domain, secure, httponly) # type: ignore # noqa: E501
self.headers.add('Set-Cookie', cookie.output(header=''))
def delete_cookie(self, key: str, path: str='/', domain: Optional[str]=None) -> None:
"""Delete a cookie (set to expire immediately)."""
self.set_cookie(key, expires=datetime.utcnow(), max_age=0, path=path, domain=domain)
async def add_etag(self, overwrite: bool=False, weak: bool=False) -> None:
if overwrite or 'etag' not in self.headers:
self.set_etag(md5((await self.get_data())).hexdigest(), weak) # type: ignore
def get_etag(self) -> Tuple[Optional[str], Optional[bool]]:
etag = self.headers.get('ETag')
if etag is None:
return None, None
else:
weak = False
if etag.upper().startswith('W/'):
etag = etag[2:]
return etag.strip('"'), weak
def set_etag(self, etag: str, weak: bool=False) -> None:
if weak:
self.headers['ETag'] = f"W/\"{etag}\""
else:
self.headers['ETag'] = f"\"{etag}\""
@property
def access_control(self) -> ResponseAccessControl:
def on_update(value: ResponseAccessControl) -> None:
self.access_control = value
return ResponseAccessControl.from_headers(
self.headers.get('Access-Control-Allow-Credentials', ''),
self.headers.get('Access-Control-Allow-Headers', ''),
self.headers.get('Access-Control-Allow-Methods', ''),
self.headers.get('Access-Control-Allow-Origin', ''),
self.headers.get('Access-Control-Expose-Headers', ''),
self.headers.get('Access-Control-Max-Age', ''),
on_update=on_update,
)
@access_control.setter
def access_control(self, value: ResponseAccessControl) -> None:
max_age = value.max_age
if max_age is None:
self.headers.pop('Access-Control-Max-Age', None)
else:
self.headers['Access-Control-Max-Age'] = max_age
if value.allow_credentials:
self.headers['Access-Control-Allow-Credentials'] = 'true'
else:
self.headers.pop('Access-Control-Allow-Credentials', None)
self._set_or_pop_header('Access-Control-Allow-Headers', value.allow_headers.to_header())
self._set_or_pop_header('Access-Control-Allow-Methods', value.allow_methods.to_header())
self._set_or_pop_header('Access-Control-Allow-Origin', value.allow_origin.to_header())
self._set_or_pop_header('Access-Control-Expose-Headers', value.expose_headers.to_header())
@property
def accept_ranges(self) -> Optional[str]:
return self.headers.get('Accept-Ranges')
@accept_ranges.setter
def accept_ranges(self, value: str) -> None:
self.headers['Accept-Ranges'] = value
@property
def age(self) -> Optional[int]:
try:
value = self.headers.get('Age', '')
except (TypeError, ValueError):
return None
return int(value) if value > 0 else None
@age.setter
def age(self, value: Union[int, timedelta]) -> None:
if isinstance(value, timedelta):
self.headers['Age'] = str(value.total_seconds())
else:
self.headers['Age'] = str(value)
@property
def allow(self) -> HeaderSet:
def on_update(header_set: HeaderSet) -> None:
self.allow = header_set
return HeaderSet.from_header(self.headers.get('Allow', ''), on_update=on_update)
@allow.setter
def allow(self, value: HeaderSet) -> None:
self._set_or_pop_header('Allow', value.to_header())
@property
def cache_control(self) -> ResponseCacheControl:
def on_update(cache_control: ResponseCacheControl) -> None:
self.cache_control = cache_control
return ResponseCacheControl.from_header(self.headers.get('Cache-Control', ''), on_update) # type: ignore # noqa: E501
@cache_control.setter
def cache_control(self, value: ResponseCacheControl) -> None:
self._set_or_pop_header('Cache-Control', value.to_header())
@property
def content_encoding(self) -> Optional[str]:
return self.headers.get('Content-Encoding')
@content_encoding.setter
def content_encoding(self, value: str) -> None:
self.headers['Content-Encoding'] = value
@property
def content_language(self) -> HeaderSet:
def on_update(header_set: HeaderSet) -> None:
self.content_language = header_set
return HeaderSet.from_header(self.headers.get('Content-Language', ''), on_update=on_update)
@content_language.setter
def content_language(self, value: HeaderSet) -> None:
self._set_or_pop_header('Content-Language', value.to_header())
@property
def content_length(self) -> Optional[int]:
try:
return int(self.headers.get('Content-Length'))
except (ValueError, TypeError):
return None
@content_length.setter
def content_length(self, value: int) -> None:
self.headers['Content-Length'] = str(value)
@property
def content_location(self) -> Optional[str]:
return self.headers.get('Content-Location')
@content_location.setter
def content_location(self, value: str) -> None:
self.headers['Content-Location'] = value
@property
def content_md5(self) -> Optional[str]:
return self.headers.get('Content-MD5')
@content_md5.setter
def content_md5(self, value: str) -> None:
self.headers['Content-MD5'] = value
@property
def content_range(self) -> ContentRange:
def on_update(cache_range: ContentRange) -> None:
self.content_range = cache_range
return ContentRange.from_header(self.headers.get('Content-Range', ''), on_update)
@content_range.setter
def content_range(self, value: ContentRange) -> None:
self._set_or_pop_header('Content-Range', value.to_header())
@property
def content_type(self) -> Optional[str]:
return self.headers.get('Content-Type')
@content_type.setter
def content_type(self, value: str) -> None:
self.headers['Content-Type'] = value
@property
def date(self) -> Optional[datetime]:
try:
return parsedate_to_datetime(self.headers.get('Date', ''))
except TypeError: # Not a date format
return None
@date.setter
def date(self, value: datetime) -> None:
self.headers['Date'] = format_date_time(value.timestamp())
@property
def expires(self) -> Optional[datetime]:
try:
return parsedate_to_datetime(self.headers.get('Expires', ''))
except TypeError: # Not a date format
return None
@expires.setter
def expires(self, value: datetime) -> None:
self.headers['Expires'] = format_date_time(value.timestamp())
@property
def last_modified(self) -> Optional[datetime]:
try:
return parsedate_to_datetime(self.headers.get('Last-Modified', ''))
except TypeError: # Not a date format
return None
@last_modified.setter
def last_modified(self, value: datetime) -> None:
self.headers['Last-Modified'] = format_date_time(value.timestamp())
@property
def location(self) -> Optional[str]:
return self.headers.get('Location')
@location.setter
def location(self, value: str) -> None:
self.headers['Location'] = value
@property
def referrer(self) -> Optional[str]:
return self.headers.get('Referer')
@referrer.setter
def referrer(self, value: str) -> None:
self.headers['Referer'] = value
@property
def retry_after(self) -> Optional[datetime]:
value = self.headers.get('Retry-After', '')
if value.isdigit():
return datetime.utcnow() + timedelta(seconds=int(value))
else:
try:
return parsedate_to_datetime(value)
except TypeError:
return None
@retry_after.setter
def retry_after(self, value: Union[datetime, int]) -> None:
if isinstance(value, datetime):
self.headers['Retry-After'] = format_date_time(value.timestamp())
else:
self.headers['Retry-After'] = str(value)
@property
def vary(self) -> HeaderSet:
def on_update(header_set: HeaderSet) -> None:
self.vary = header_set
return HeaderSet.from_header(self.headers.get('Vary', ''), on_update=on_update)
@vary.setter
def vary(self, value: HeaderSet) -> None:
self._set_or_pop_header('Vary', value.to_header())
async def _load_json_data(self) -> str:
"""Return the data after decoding."""
return await self.get_data(raw=False)
def _set_or_pop_header(self, key: str, value: str) -> None:
if value == '':
self.headers.pop(key, None)
else:
self.headers[key] = value
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.status_code})"