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

708 lines
22 KiB
Python

import codecs
import io
import re
from cgi import parse_header
from datetime import datetime
from email.utils import parsedate_to_datetime
from functools import wraps
from shutil import copyfileobj
from typing import (
Any, BinaryIO, Callable, Dict, Iterable, List, NamedTuple, Optional, Set, Type, Union,
)
from urllib.request import parse_http_list, parse_keqv_list
from wsgiref.handlers import format_date_time
from multidict import CIMultiDict as AIOCIMultiDict, MultiDict as AIOMultiDict
class _WerkzeugMultidictMixin:
def get(self, key: str, default: Any=None, type: Any=None) -> Any:
value = super().get(key, default) # type: ignore
if type is not None:
try:
value = type(value)
except ValueError:
value = default
return value
def getlist(self, key: str, type: Any=None) -> List[Any]:
values = self.getall(key, []) # type: ignore
if type is not None:
result = []
for value in values:
try:
result.append(type(value))
except ValueError:
pass
return result
else:
return values
def to_dict(self, flat: bool=True) -> Dict[Any, Any]:
"""Convert the multidict to a plain dictionary.
Arguments:
flat: If True only return the a value for each key, if
False return all values as lists.
"""
if flat:
return {key: value for key, value in self.items()} # type: ignore
else:
return {key: self.getall(key) for key in self} # type: ignore
class MultiDict(_WerkzeugMultidictMixin, AIOMultiDict): # type: ignore
pass
class CIMultiDict(_WerkzeugMultidictMixin, AIOCIMultiDict): # type: ignore
pass
def _unicodify(value: Any) -> str:
try:
return value.decode()
except AttributeError:
return str(value)
class Headers(CIMultiDict):
# Headers should accept byte keys and values but convert them to
# unicode whilst stored. Note only a select few methods do this,
# the others will error if presented with byte keys.
def add(self, key: Any, value: Any, **kwargs: Any) -> None:
if kwargs:
segments = [value] if value is not None else []
for name, arg in kwargs.items():
if arg is None:
segments.append(name)
else:
segments.append(f"{name}={arg}")
value = "; ".join(segments)
else:
value = _unicodify(value)
super().add(_unicodify(key), value)
def __setitem__(self, key: Any, value: Any) -> None:
super().__setitem__(_unicodify(key), _unicodify(value))
def setdefault(self, key: Any, default: Optional[Any]=None) -> Optional[str]:
return super().setdefault(_unicodify(key), _unicodify(default))
class FileStorage:
"""A thin wrapper over incoming files."""
def __init__(
self,
stream: BinaryIO=None,
filename: str=None,
name: str=None,
content_type: str=None,
headers: Dict=None,
) -> None:
self.name = name
self.stream = stream or io.BytesIO()
self.filename = filename
if headers is None:
headers = {}
self.headers = headers
if content_type is not None:
headers['Content-Type'] = content_type
@property
def content_type(self) -> Optional[str]:
"""The content-type sent in the header."""
return self.headers.get('Content-Type')
@property
def content_length(self) -> int:
"""The content-length sent in the header."""
return int(self.headers.get('content-length', 0))
@property
def mimetype(self) -> str:
"""Returns the mimetype parsed from the Content-Type header."""
return parse_header(self.headers.get('Content-Type'))[0]
@property
def mimetype_params(self) -> Dict[str, str]:
"""Returns the params parsed from the Content-Type header."""
return parse_header(self.headers.get('Content-Type'))[1]
def save(self, destination: BinaryIO, buffer_size: int=16384) -> None:
"""Save the file to the destination.
Arguments:
destination: A filename (str) or file object to write to.
buffer_size: Buffer size as used as length in
:func:`shutil.copyfileobj`.
"""
close_destination = False
if isinstance(destination, str):
destination = open(destination, 'wb')
close_destination = True
try:
copyfileobj(self.stream, destination, buffer_size)
finally:
if close_destination:
destination.close()
def close(self) -> None:
try:
self.stream.close()
except Exception:
pass
def __bool__(self) -> bool:
return bool(self.filename)
def __getattr__(self, name: str) -> Any:
return getattr(self.stream, name)
def __iter__(self) -> Iterable[bytes]:
return iter(self.stream)
def __repr__(self) -> str:
return f"<{self.__class__.__name__}: {self.filename} ({self.content_type}))>"
class Authorization:
def __init__(
self,
cnonce: Optional[str]=None,
nc: Optional[str]=None,
nonce: Optional[str]=None,
opaque: Optional[str]=None,
password: Optional[str]=None,
qop: Optional[str]=None,
realm: Optional[str]=None,
response: Optional[str]=None,
uri: Optional[str]=None,
username: Optional[str]=None,
) -> None:
self.cnonce = cnonce
self.nc = nc
self.nonce = nonce
self.opaque = opaque
self.password = password
self.qop = qop
self.realm = realm
self.response = response
self.uri = uri
self.username = username
class AcceptOption(NamedTuple):
value: str
quality: float
parameters: dict
class Accept:
def __init__(self, header_value: str) -> None:
self.options: List[AcceptOption] = []
for accept_option in parse_http_list(header_value):
option, params = parse_header(accept_option)
quality = float(params.pop('q', 1.0))
self.options.append(AcceptOption(option, quality, params))
def best_match(self, matches: List[str], default: Optional[str]=None) -> Optional[str]:
best_match = AcceptOption(default, -1.0, {})
for possible_match in matches:
for option in self.options:
if (
self._values_match(possible_match, option.value) and
option.quality > best_match.quality
):
best_match = AcceptOption(possible_match, option.quality, {})
return best_match.value
def quality(self, key: str) -> float:
for option in self.options:
if self._values_match(key, option.value):
return option.quality
return 0.0
def _values_match(self, lhs: str, rhs: str) -> bool:
return rhs == '*' or lhs.lower() == rhs.lower()
def __getitem__(self, key: str) -> float:
return self.quality(key)
def __contains__(self, key: str) -> bool:
for option in self.options:
if self._values_match(key, option.value):
return True
return False
class CharsetAccept(Accept):
def _values_match(self, lhs: str, rhs: str) -> bool:
try:
lhs_normalised = codecs.lookup(lhs).name
except LookupError:
lhs_normalised = lhs.lower()
try:
rhs_normalised = codecs.lookup(rhs).name
except LookupError:
rhs_normalised = rhs.lower()
return rhs == '*' or lhs_normalised == rhs_normalised
class LanguageAccept(Accept):
def _values_match(self, lhs: str, rhs: str) -> bool:
lhs_normalised = re.split(r'[_-]', lhs.lower())
rhs_normalised = re.split(r'[_-]', rhs.lower())
return rhs == '*' or lhs_normalised == rhs_normalised
class MIMEAccept(Accept):
def _values_match(self, lhs: str, rhs: str) -> bool:
if rhs == '*':
rhs_normalised = ['*', '*']
else:
rhs_normalised = rhs.lower().split('/', 1)
if lhs == '*':
lhs_normalised = ['*', '*']
else:
try:
lhs_normalised = lhs.lower().split('/', 1)
except ValueError:
return False
full_wildcard_allowed = (
lhs_normalised[0] == lhs_normalised[1] == '*' or
rhs_normalised[0] == rhs_normalised[1] == '*'
)
wildcard_allowed = (
lhs_normalised[0] == rhs_normalised[0] and
(lhs_normalised[1] == '*' or rhs_normalised[1] == '*')
)
match_allowed = lhs_normalised == rhs_normalised
return full_wildcard_allowed or wildcard_allowed or match_allowed
class _CacheDirective:
def __init__(self, name: str, converter: Callable) -> None:
self.name = name
self.converter = converter
def __get__(self, instance: object, owner: type=None) -> Any:
if instance is None:
return self
result = instance._directives[self.name] # type: ignore
return self.converter(result)
def __set__(self, instance: object, value: Any) -> None:
instance._directives[self.name] = self.converter(value) # type: ignore
if instance._on_update is not None: # type: ignore
instance._on_update(instance) # type: ignore
class _CacheControl:
no_cache = _CacheDirective('no-cache', bool)
no_store = _CacheDirective('no-store', bool)
no_transform = _CacheDirective('no-transform', bool)
max_age = _CacheDirective('max-age', int)
def __init__(self, on_update: Optional[Callable]=None) -> None:
self._on_update = on_update
self._directives: Dict[str, Any] = {}
@classmethod
def from_header(
cls: Type['_CacheControl'],
header: str,
on_update: Optional[Callable]=None,
) -> '_CacheControl':
cache_control = cls(on_update)
for item in parse_http_list(header):
if '=' in item:
for key, value in parse_keqv_list([item]).items():
cache_control._directives[key] = value
else:
cache_control._directives[item] = True
return cache_control
def to_header(self) -> str:
header = ''
for directive, value in self._directives.items():
if isinstance(value, bool):
if value:
header += f"{directive},"
else:
header += f"{directive}={value},"
return header.strip(',')
class RequestCacheControl(_CacheControl):
max_stale = _CacheDirective('max-stale', int)
min_fresh = _CacheDirective('min-fresh', int)
no_transform = _CacheDirective('no-transform', bool)
only_if_cached = _CacheDirective('only-if-cached', bool)
class ResponseCacheControl(_CacheControl):
must_revalidate = _CacheDirective('must-revalidate', bool)
private = _CacheDirective('private', bool)
proxy_revalidate = _CacheDirective('proxy-revalidate', bool)
public = _CacheDirective('public', bool)
s_maxage = _CacheDirective('s-maxage', int)
class ETags:
def __init__(
self,
weak: Optional[Set[str]]=None,
strong: Optional[Set[str]]=None,
star: bool=False,
) -> None:
self.weak = weak or set()
self.strong = strong or set()
self.star = star
def __contains__(self, etag: str) -> bool:
return self.star or etag in self.strong
@classmethod
def from_header(cls: Type['ETags'], header: str) -> 'ETags':
header = header.strip()
weak = set()
strong = set()
if header == '*':
return ETags(star=True)
else:
for item in parse_http_list(header):
if item.upper().startswith('W/'):
weak.add(item[2:].strip('"'))
else:
strong.add(item.strip('"'))
return ETags(weak, strong)
def to_header(self) -> str:
if self.star:
return '*'
else:
header = ''
for tag in self.weak:
header += f"W/\"{tag}\","
for tag in self.strong:
header += f"\"{tag}\","
return header.strip(',')
class IfRange:
def __init__(self, etag: Optional[str]=None, date: Optional[datetime]=None) -> None:
self.etag = etag
self.date = date
@classmethod
def from_header(cls: Type['IfRange'], header: str) -> 'IfRange':
try:
return IfRange(date=parsedate_to_datetime(header))
except TypeError: # Not a date format
return IfRange(etag=header.strip('"'))
def to_header(self) -> str:
if self.etag is not None:
return f"\"{self.etag}\""
elif self.date is not None:
return format_date_time(self.date.timestamp())
else:
return ''
class RangeSet(NamedTuple):
begin: int
end: Optional[int]
class Range:
def __init__(self, units: str, ranges: List[RangeSet]) -> None:
self.units = units
self.ranges = ranges
@classmethod
def from_header(cls: Type['Range'], header: str) -> 'Range':
try:
units, raw_ranges = header.split('=', 1)
except ValueError:
return cls('', [])
units = units.strip().lower()
ranges = []
for range_set in parse_http_list(raw_ranges):
if range_set.startswith('-'):
ranges.append(RangeSet(int(range_set), None))
elif range_set.endswith('-'):
ranges.append(RangeSet(int(range_set[:-1]), None))
elif '-' in range_set:
begin, end = range_set.split('-')
ranges.append(RangeSet(int(begin), int(end)))
else:
ranges.append(RangeSet(0, int(range_set)))
return Range(units, ranges)
def to_header(self) -> str:
header = f"{self.units}="
for range_set in self.ranges:
header += f"{range_set.begin}"
if range_set.begin >= 0:
header += "-"
if range_set.end is not None:
header += f"{range_set.end}"
header += ','
return header.strip(',')
def _on_update(method: Callable) -> Callable:
@wraps(method)
def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
result = method(self, *args, **kwargs)
if self.on_update is not None:
self.on_update(self)
return result
return wrapper
class HeaderSet(set):
def __init__(self, *args: Any, on_update: Optional[Callable]=None, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self.on_update = on_update
def to_header(self) -> str:
header = ', '.join(self)
return header.strip(',')
@classmethod
def from_header(
cls: Type['HeaderSet'],
header: str,
on_update: Optional[Callable]=None,
) -> 'HeaderSet':
items = {item for item in parse_http_list(header)}
return cls(items, on_update=on_update)
add = _on_update(set.add)
clear = _on_update(set.clear)
pop = _on_update(set.pop)
remove = _on_update(set.remove)
update = _on_update(set.update)
class _ContentRangeSpec:
def __init__(self, name: str, converter: Callable) -> None:
self.name = name
self.converter = converter
def __get__(self, instance: object, owner: type=None) -> Any:
if instance is None:
return self
result = instance._specs.get(self.name) # type: ignore
if result is not None:
return self.converter(result)
else:
return None
def __set__(self, instance: object, value: Any) -> None:
instance._specs[self.name] = value # type: ignore
if instance._on_update is not None: # type: ignore
instance._on_update(instance) # type: ignore
class ContentRange:
units = _ContentRangeSpec('units', str)
start = _ContentRangeSpec('start', int)
stop = _ContentRangeSpec('stop', int)
length = _ContentRangeSpec('length', int)
def __init__(
self,
units: Optional[str],
start: Optional[int],
stop: Optional[int],
length: Optional[Union[int, str]]=None,
on_update: Optional[Callable]=None,
) -> None:
self._on_update = on_update
self._specs: Dict[str, Any] = {}
self.units = units
self.start = start
self.stop = stop
self.length = length
@classmethod
def from_header(
cls: Type['ContentRange'],
header: str,
on_update: Optional[Callable]=None,
) -> 'ContentRange':
try:
units, range_spec = header.split(None, 1)
except ValueError:
return cls(None, None, None)
try:
range_, length = range_spec.split('/', 1)
length = int(length) # type: ignore
except ValueError:
return cls(None, None, None)
if range_ == '*':
return cls(units, None, None, length, on_update)
try:
start, stop = range_.split('-', 1)
start = int(start) # type: ignore
stop = int(stop) # type: ignore
except ValueError:
return cls(None, None, None)
return cls(units, start, stop, length, on_update) # type: ignore
def to_header(self) -> str:
if self.units is None:
return ''
length = self.length or '*'
if self.start is None:
return f"{self.units} */{length}"
else:
return f"{self.units} {self.start}-{self.stop}/{length}"
def __eq__(self, other: object) -> bool:
return self.__class__ == other.__class__ and self._specs == other._specs # type: ignore
def __ne__(self, other: object) -> bool:
return not self.__eq__(other)
class RequestAccessControl:
def __init__(self, origin: str, request_headers: HeaderSet, request_method: str) -> None:
self.origin = origin
self.request_headers = request_headers
self.request_method = request_method
@classmethod
def from_headers(
cls: Type['RequestAccessControl'],
origin_header: str,
request_headers_header: str,
request_method_header: str,
) -> 'RequestAccessControl':
request_headers = HeaderSet.from_header(request_headers_header)
return cls(origin_header, request_headers, request_method_header)
class _AccessControlDescriptor:
def __init__(self, name: str) -> None:
self.name = name
def __get__(self, instance: object, owner: type=None) -> Any:
if instance is None:
return self
return instance._controls[self.name] # type: ignore
def __set__(self, instance: object, value: Any) -> None:
header_set = HeaderSet(value, on_update=instance.on_update) # type: ignore
instance._controls[self.name] = header_set # type: ignore
if instance.on_update is not None: # type: ignore
instance.on_update() # type: ignore
class ResponseAccessControl:
allow_headers = _AccessControlDescriptor('allow_headers')
allow_methods = _AccessControlDescriptor('allow_methods')
allow_origin = _AccessControlDescriptor('allow_origin')
expose_headers = _AccessControlDescriptor('expose_headers')
def __init__(
self,
allow_credentials: Optional[bool],
allow_headers: HeaderSet,
allow_methods: HeaderSet,
allow_origin: HeaderSet,
expose_headers: HeaderSet,
max_age: Optional[float],
on_update: Optional[Callable]=None,
) -> None:
self._on_update = None
self._controls: Dict[str, Any] = {}
self.allow_credentials = allow_credentials
self.allow_headers = allow_headers
self.allow_methods = allow_methods
self.allow_origin = allow_origin
self.expose_headers = expose_headers
self.max_age = max_age
self._on_update = on_update
@property
def allow_credentials(self) -> bool:
return self._controls['allow_credentials'] is True
@allow_credentials.setter
def allow_credentials(self, value: Optional[bool]=None) -> None:
self._controls['allow_credentials'] = value
self.on_update()
@property
def max_age(self) -> Optional[float]:
return self._controls['max_age']
@max_age.setter
def max_age(self, value: Optional[float]=None) -> None:
try:
value = float(value)
except (TypeError, ValueError):
value = None
self._controls['max_age'] = value
self.on_update()
@classmethod
def from_headers(
cls: Type['ResponseAccessControl'],
allow_credentials_header: str,
allow_headers_header: str,
allow_methods_header: str,
allow_origin_header: str,
expose_headers_header: str,
max_age_header: str,
on_update: Optional[Callable]=None,
) -> 'ResponseAccessControl':
allow_credentials = allow_credentials_header == 'true'
allow_headers = HeaderSet.from_header(allow_headers_header)
allow_methods = HeaderSet.from_header(allow_methods_header)
allow_origin = HeaderSet.from_header(allow_origin_header)
expose_headers = HeaderSet.from_header(expose_headers_header)
try:
max_age = float(max_age_header)
except (ValueError, TypeError):
max_age = None
return cls(
allow_credentials, allow_headers, allow_methods, allow_origin, expose_headers, max_age,
on_update,
)
def on_update(self, _: Any=None) -> None:
if self._on_update is not None:
self._on_update(self)