| | import asyncio |
| | import datetime |
| | import io |
| | import re |
| | import socket |
| | import string |
| | import tempfile |
| | import types |
| | import warnings |
| | from http.cookies import SimpleCookie |
| | from types import MappingProxyType |
| | from typing import ( |
| | TYPE_CHECKING, |
| | Any, |
| | Dict, |
| | Final, |
| | Iterator, |
| | Mapping, |
| | MutableMapping, |
| | Optional, |
| | Pattern, |
| | Tuple, |
| | Union, |
| | cast, |
| | ) |
| | from urllib.parse import parse_qsl |
| |
|
| | import attr |
| | from multidict import ( |
| | CIMultiDict, |
| | CIMultiDictProxy, |
| | MultiDict, |
| | MultiDictProxy, |
| | MultiMapping, |
| | ) |
| | from yarl import URL |
| |
|
| | from . import hdrs |
| | from .abc import AbstractStreamWriter |
| | from .helpers import ( |
| | _SENTINEL, |
| | DEBUG, |
| | ETAG_ANY, |
| | LIST_QUOTED_ETAG_RE, |
| | ChainMapProxy, |
| | ETag, |
| | HeadersMixin, |
| | parse_http_date, |
| | reify, |
| | sentinel, |
| | set_exception, |
| | ) |
| | from .http_parser import RawRequestMessage |
| | from .http_writer import HttpVersion |
| | from .multipart import BodyPartReader, MultipartReader |
| | from .streams import EmptyStreamReader, StreamReader |
| | from .typedefs import ( |
| | DEFAULT_JSON_DECODER, |
| | JSONDecoder, |
| | LooseHeaders, |
| | RawHeaders, |
| | StrOrURL, |
| | ) |
| | from .web_exceptions import HTTPRequestEntityTooLarge |
| | from .web_response import StreamResponse |
| |
|
| | __all__ = ("BaseRequest", "FileField", "Request") |
| |
|
| |
|
| | if TYPE_CHECKING: |
| | from .web_app import Application |
| | from .web_protocol import RequestHandler |
| | from .web_urldispatcher import UrlMappingMatchInfo |
| |
|
| |
|
| | @attr.s(auto_attribs=True, frozen=True, slots=True) |
| | class FileField: |
| | name: str |
| | filename: str |
| | file: io.BufferedReader |
| | content_type: str |
| | headers: CIMultiDictProxy[str] |
| |
|
| |
|
| | _TCHAR: Final[str] = string.digits + string.ascii_letters + r"!#$%&'*+.^_`|~-" |
| | |
| |
|
| | _TOKEN: Final[str] = rf"[{_TCHAR}]+" |
| |
|
| | _QDTEXT: Final[str] = r"[{}]".format( |
| | r"".join(chr(c) for c in (0x09, 0x20, 0x21) + tuple(range(0x23, 0x7F))) |
| | ) |
| | |
| | |
| |
|
| | _QUOTED_PAIR: Final[str] = r"\\[\t !-~]" |
| |
|
| | _QUOTED_STRING: Final[str] = r'"(?:{quoted_pair}|{qdtext})*"'.format( |
| | qdtext=_QDTEXT, quoted_pair=_QUOTED_PAIR |
| | ) |
| |
|
| | _FORWARDED_PAIR: Final[str] = ( |
| | r"({token})=({token}|{quoted_string})(:\d{{1,4}})?".format( |
| | token=_TOKEN, quoted_string=_QUOTED_STRING |
| | ) |
| | ) |
| |
|
| | _QUOTED_PAIR_REPLACE_RE: Final[Pattern[str]] = re.compile(r"\\([\t !-~])") |
| | |
| |
|
| | _FORWARDED_PAIR_RE: Final[Pattern[str]] = re.compile(_FORWARDED_PAIR) |
| |
|
| | |
| | |
| | |
| |
|
| |
|
| | class BaseRequest(MutableMapping[str, Any], HeadersMixin): |
| |
|
| | POST_METHODS = { |
| | hdrs.METH_PATCH, |
| | hdrs.METH_POST, |
| | hdrs.METH_PUT, |
| | hdrs.METH_TRACE, |
| | hdrs.METH_DELETE, |
| | } |
| |
|
| | ATTRS = HeadersMixin.ATTRS | frozenset( |
| | [ |
| | "_message", |
| | "_protocol", |
| | "_payload_writer", |
| | "_payload", |
| | "_headers", |
| | "_method", |
| | "_version", |
| | "_rel_url", |
| | "_post", |
| | "_read_bytes", |
| | "_state", |
| | "_cache", |
| | "_task", |
| | "_client_max_size", |
| | "_loop", |
| | "_transport_sslcontext", |
| | "_transport_peername", |
| | ] |
| | ) |
| | _post: Optional[MultiDictProxy[Union[str, bytes, FileField]]] = None |
| | _read_bytes: Optional[bytes] = None |
| |
|
| | def __init__( |
| | self, |
| | message: RawRequestMessage, |
| | payload: StreamReader, |
| | protocol: "RequestHandler", |
| | payload_writer: AbstractStreamWriter, |
| | task: "asyncio.Task[None]", |
| | loop: asyncio.AbstractEventLoop, |
| | *, |
| | client_max_size: int = 1024**2, |
| | state: Optional[Dict[str, Any]] = None, |
| | scheme: Optional[str] = None, |
| | host: Optional[str] = None, |
| | remote: Optional[str] = None, |
| | ) -> None: |
| | self._message = message |
| | self._protocol = protocol |
| | self._payload_writer = payload_writer |
| |
|
| | self._payload = payload |
| | self._headers: CIMultiDictProxy[str] = message.headers |
| | self._method = message.method |
| | self._version = message.version |
| | self._cache: Dict[str, Any] = {} |
| | url = message.url |
| | if url.absolute: |
| | if scheme is not None: |
| | url = url.with_scheme(scheme) |
| | if host is not None: |
| | url = url.with_host(host) |
| | |
| | |
| | |
| | self._cache["url"] = url |
| | self._cache["host"] = url.host |
| | self._cache["scheme"] = url.scheme |
| | self._rel_url = url.relative() |
| | else: |
| | self._rel_url = url |
| | if scheme is not None: |
| | self._cache["scheme"] = scheme |
| | if host is not None: |
| | self._cache["host"] = host |
| |
|
| | self._state = {} if state is None else state |
| | self._task = task |
| | self._client_max_size = client_max_size |
| | self._loop = loop |
| |
|
| | transport = protocol.transport |
| | assert transport is not None |
| | self._transport_sslcontext = transport.get_extra_info("sslcontext") |
| | self._transport_peername = transport.get_extra_info("peername") |
| |
|
| | if remote is not None: |
| | self._cache["remote"] = remote |
| |
|
| | def clone( |
| | self, |
| | *, |
| | method: Union[str, _SENTINEL] = sentinel, |
| | rel_url: Union[StrOrURL, _SENTINEL] = sentinel, |
| | headers: Union[LooseHeaders, _SENTINEL] = sentinel, |
| | scheme: Union[str, _SENTINEL] = sentinel, |
| | host: Union[str, _SENTINEL] = sentinel, |
| | remote: Union[str, _SENTINEL] = sentinel, |
| | client_max_size: Union[int, _SENTINEL] = sentinel, |
| | ) -> "BaseRequest": |
| | """Clone itself with replacement some attributes. |
| | |
| | Creates and returns a new instance of Request object. If no parameters |
| | are given, an exact copy is returned. If a parameter is not passed, it |
| | will reuse the one from the current request object. |
| | """ |
| | if self._read_bytes: |
| | raise RuntimeError("Cannot clone request after reading its content") |
| |
|
| | dct: Dict[str, Any] = {} |
| | if method is not sentinel: |
| | dct["method"] = method |
| | if rel_url is not sentinel: |
| | new_url: URL = URL(rel_url) |
| | dct["url"] = new_url |
| | dct["path"] = str(new_url) |
| | if headers is not sentinel: |
| | |
| | dct["headers"] = CIMultiDictProxy(CIMultiDict(headers)) |
| | dct["raw_headers"] = tuple( |
| | (k.encode("utf-8"), v.encode("utf-8")) |
| | for k, v in dct["headers"].items() |
| | ) |
| |
|
| | message = self._message._replace(**dct) |
| |
|
| | kwargs = {} |
| | if scheme is not sentinel: |
| | kwargs["scheme"] = scheme |
| | if host is not sentinel: |
| | kwargs["host"] = host |
| | if remote is not sentinel: |
| | kwargs["remote"] = remote |
| | if client_max_size is sentinel: |
| | client_max_size = self._client_max_size |
| |
|
| | return self.__class__( |
| | message, |
| | self._payload, |
| | self._protocol, |
| | self._payload_writer, |
| | self._task, |
| | self._loop, |
| | client_max_size=client_max_size, |
| | state=self._state.copy(), |
| | **kwargs, |
| | ) |
| |
|
| | @property |
| | def task(self) -> "asyncio.Task[None]": |
| | return self._task |
| |
|
| | @property |
| | def protocol(self) -> "RequestHandler": |
| | return self._protocol |
| |
|
| | @property |
| | def transport(self) -> Optional[asyncio.Transport]: |
| | if self._protocol is None: |
| | return None |
| | return self._protocol.transport |
| |
|
| | @property |
| | def writer(self) -> AbstractStreamWriter: |
| | return self._payload_writer |
| |
|
| | @property |
| | def client_max_size(self) -> int: |
| | return self._client_max_size |
| |
|
| | @reify |
| | def message(self) -> RawRequestMessage: |
| | warnings.warn("Request.message is deprecated", DeprecationWarning, stacklevel=3) |
| | return self._message |
| |
|
| | @reify |
| | def rel_url(self) -> URL: |
| | return self._rel_url |
| |
|
| | @reify |
| | def loop(self) -> asyncio.AbstractEventLoop: |
| | warnings.warn( |
| | "request.loop property is deprecated", DeprecationWarning, stacklevel=2 |
| | ) |
| | return self._loop |
| |
|
| | |
| |
|
| | def __getitem__(self, key: str) -> Any: |
| | return self._state[key] |
| |
|
| | def __setitem__(self, key: str, value: Any) -> None: |
| | self._state[key] = value |
| |
|
| | def __delitem__(self, key: str) -> None: |
| | del self._state[key] |
| |
|
| | def __len__(self) -> int: |
| | return len(self._state) |
| |
|
| | def __iter__(self) -> Iterator[str]: |
| | return iter(self._state) |
| |
|
| | |
| |
|
| | @reify |
| | def secure(self) -> bool: |
| | """A bool indicating if the request is handled with SSL.""" |
| | return self.scheme == "https" |
| |
|
| | @reify |
| | def forwarded(self) -> Tuple[Mapping[str, str], ...]: |
| | """A tuple containing all parsed Forwarded header(s). |
| | |
| | Makes an effort to parse Forwarded headers as specified by RFC 7239: |
| | |
| | - It adds one (immutable) dictionary per Forwarded 'field-value', ie |
| | per proxy. The element corresponds to the data in the Forwarded |
| | field-value added by the first proxy encountered by the client. Each |
| | subsequent item corresponds to those added by later proxies. |
| | - It checks that every value has valid syntax in general as specified |
| | in section 4: either a 'token' or a 'quoted-string'. |
| | - It un-escapes found escape sequences. |
| | - It does NOT validate 'by' and 'for' contents as specified in section |
| | 6. |
| | - It does NOT validate 'host' contents (Host ABNF). |
| | - It does NOT validate 'proto' contents for valid URI scheme names. |
| | |
| | Returns a tuple containing one or more immutable dicts |
| | """ |
| | elems = [] |
| | for field_value in self._message.headers.getall(hdrs.FORWARDED, ()): |
| | length = len(field_value) |
| | pos = 0 |
| | need_separator = False |
| | elem: Dict[str, str] = {} |
| | elems.append(types.MappingProxyType(elem)) |
| | while 0 <= pos < length: |
| | match = _FORWARDED_PAIR_RE.match(field_value, pos) |
| | if match is not None: |
| | if need_separator: |
| | |
| | pos = field_value.find(",", pos) |
| | else: |
| | name, value, port = match.groups() |
| | if value[0] == '"': |
| | |
| | value = _QUOTED_PAIR_REPLACE_RE.sub(r"\1", value[1:-1]) |
| | if port: |
| | value += port |
| | elem[name.lower()] = value |
| | pos += len(match.group(0)) |
| | need_separator = True |
| | elif field_value[pos] == ",": |
| | need_separator = False |
| | elem = {} |
| | elems.append(types.MappingProxyType(elem)) |
| | pos += 1 |
| | elif field_value[pos] == ";": |
| | need_separator = False |
| | pos += 1 |
| | elif field_value[pos] in " \t": |
| | |
| | |
| | |
| | pos += 1 |
| | else: |
| | |
| | pos = field_value.find(",", pos) |
| | return tuple(elems) |
| |
|
| | @reify |
| | def scheme(self) -> str: |
| | """A string representing the scheme of the request. |
| | |
| | Hostname is resolved in this order: |
| | |
| | - overridden value by .clone(scheme=new_scheme) call. |
| | - type of connection to peer: HTTPS if socket is SSL, HTTP otherwise. |
| | |
| | 'http' or 'https'. |
| | """ |
| | if self._transport_sslcontext: |
| | return "https" |
| | else: |
| | return "http" |
| |
|
| | @reify |
| | def method(self) -> str: |
| | """Read only property for getting HTTP method. |
| | |
| | The value is upper-cased str like 'GET', 'POST', 'PUT' etc. |
| | """ |
| | return self._method |
| |
|
| | @reify |
| | def version(self) -> HttpVersion: |
| | """Read only property for getting HTTP version of request. |
| | |
| | Returns aiohttp.protocol.HttpVersion instance. |
| | """ |
| | return self._version |
| |
|
| | @reify |
| | def host(self) -> str: |
| | """Hostname of the request. |
| | |
| | Hostname is resolved in this order: |
| | |
| | - overridden value by .clone(host=new_host) call. |
| | - HOST HTTP header |
| | - socket.getfqdn() value |
| | |
| | For example, 'example.com' or 'localhost:8080'. |
| | |
| | For historical reasons, the port number may be included. |
| | """ |
| | host = self._message.headers.get(hdrs.HOST) |
| | if host is not None: |
| | return host |
| | return socket.getfqdn() |
| |
|
| | @reify |
| | def remote(self) -> Optional[str]: |
| | """Remote IP of client initiated HTTP request. |
| | |
| | The IP is resolved in this order: |
| | |
| | - overridden value by .clone(remote=new_remote) call. |
| | - peername of opened socket |
| | """ |
| | if self._transport_peername is None: |
| | return None |
| | if isinstance(self._transport_peername, (list, tuple)): |
| | return str(self._transport_peername[0]) |
| | return str(self._transport_peername) |
| |
|
| | @reify |
| | def url(self) -> URL: |
| | """The full URL of the request.""" |
| | |
| | |
| | return URL.build(scheme=self.scheme, authority=self.host).join(self._rel_url) |
| |
|
| | @reify |
| | def path(self) -> str: |
| | """The URL including *PATH INFO* without the host or scheme. |
| | |
| | E.g., ``/app/blog`` |
| | """ |
| | return self._rel_url.path |
| |
|
| | @reify |
| | def path_qs(self) -> str: |
| | """The URL including PATH_INFO and the query string. |
| | |
| | E.g, /app/blog?id=10 |
| | """ |
| | return str(self._rel_url) |
| |
|
| | @reify |
| | def raw_path(self) -> str: |
| | """The URL including raw *PATH INFO* without the host or scheme. |
| | |
| | Warning, the path is unquoted and may contains non valid URL characters |
| | |
| | E.g., ``/my%2Fpath%7Cwith%21some%25strange%24characters`` |
| | """ |
| | return self._message.path |
| |
|
| | @reify |
| | def query(self) -> "MultiMapping[str]": |
| | """A multidict with all the variables in the query string.""" |
| | return self._rel_url.query |
| |
|
| | @reify |
| | def query_string(self) -> str: |
| | """The query string in the URL. |
| | |
| | E.g., id=10 |
| | """ |
| | return self._rel_url.query_string |
| |
|
| | @reify |
| | def headers(self) -> CIMultiDictProxy[str]: |
| | """A case-insensitive multidict proxy with all headers.""" |
| | return self._headers |
| |
|
| | @reify |
| | def raw_headers(self) -> RawHeaders: |
| | """A sequence of pairs for all headers.""" |
| | return self._message.raw_headers |
| |
|
| | @reify |
| | def if_modified_since(self) -> Optional[datetime.datetime]: |
| | """The value of If-Modified-Since HTTP header, or None. |
| | |
| | This header is represented as a `datetime` object. |
| | """ |
| | return parse_http_date(self.headers.get(hdrs.IF_MODIFIED_SINCE)) |
| |
|
| | @reify |
| | def if_unmodified_since(self) -> Optional[datetime.datetime]: |
| | """The value of If-Unmodified-Since HTTP header, or None. |
| | |
| | This header is represented as a `datetime` object. |
| | """ |
| | return parse_http_date(self.headers.get(hdrs.IF_UNMODIFIED_SINCE)) |
| |
|
| | @staticmethod |
| | def _etag_values(etag_header: str) -> Iterator[ETag]: |
| | """Extract `ETag` objects from raw header.""" |
| | if etag_header == ETAG_ANY: |
| | yield ETag( |
| | is_weak=False, |
| | value=ETAG_ANY, |
| | ) |
| | else: |
| | for match in LIST_QUOTED_ETAG_RE.finditer(etag_header): |
| | is_weak, value, garbage = match.group(2, 3, 4) |
| | |
| | |
| | if garbage: |
| | break |
| |
|
| | yield ETag( |
| | is_weak=bool(is_weak), |
| | value=value, |
| | ) |
| |
|
| | @classmethod |
| | def _if_match_or_none_impl( |
| | cls, header_value: Optional[str] |
| | ) -> Optional[Tuple[ETag, ...]]: |
| | if not header_value: |
| | return None |
| |
|
| | return tuple(cls._etag_values(header_value)) |
| |
|
| | @reify |
| | def if_match(self) -> Optional[Tuple[ETag, ...]]: |
| | """The value of If-Match HTTP header, or None. |
| | |
| | This header is represented as a `tuple` of `ETag` objects. |
| | """ |
| | return self._if_match_or_none_impl(self.headers.get(hdrs.IF_MATCH)) |
| |
|
| | @reify |
| | def if_none_match(self) -> Optional[Tuple[ETag, ...]]: |
| | """The value of If-None-Match HTTP header, or None. |
| | |
| | This header is represented as a `tuple` of `ETag` objects. |
| | """ |
| | return self._if_match_or_none_impl(self.headers.get(hdrs.IF_NONE_MATCH)) |
| |
|
| | @reify |
| | def if_range(self) -> Optional[datetime.datetime]: |
| | """The value of If-Range HTTP header, or None. |
| | |
| | This header is represented as a `datetime` object. |
| | """ |
| | return parse_http_date(self.headers.get(hdrs.IF_RANGE)) |
| |
|
| | @reify |
| | def keep_alive(self) -> bool: |
| | """Is keepalive enabled by client?""" |
| | return not self._message.should_close |
| |
|
| | @reify |
| | def cookies(self) -> Mapping[str, str]: |
| | """Return request cookies. |
| | |
| | A read-only dictionary-like object. |
| | """ |
| | raw = self.headers.get(hdrs.COOKIE, "") |
| | parsed = SimpleCookie(raw) |
| | return MappingProxyType({key: val.value for key, val in parsed.items()}) |
| |
|
| | @reify |
| | def http_range(self) -> slice: |
| | """The content of Range HTTP header. |
| | |
| | Return a slice instance. |
| | |
| | """ |
| | rng = self._headers.get(hdrs.RANGE) |
| | start, end = None, None |
| | if rng is not None: |
| | try: |
| | pattern = r"^bytes=(\d*)-(\d*)$" |
| | start, end = re.findall(pattern, rng)[0] |
| | except IndexError: |
| | raise ValueError("range not in acceptable format") |
| |
|
| | end = int(end) if end else None |
| | start = int(start) if start else None |
| |
|
| | if start is None and end is not None: |
| | |
| | start = -end |
| | end = None |
| |
|
| | if start is not None and end is not None: |
| | |
| | end += 1 |
| |
|
| | if start >= end: |
| | raise ValueError("start cannot be after end") |
| |
|
| | if start is end is None: |
| | raise ValueError("No start or end of range specified") |
| |
|
| | return slice(start, end, 1) |
| |
|
| | @reify |
| | def content(self) -> StreamReader: |
| | """Return raw payload stream.""" |
| | return self._payload |
| |
|
| | @property |
| | def has_body(self) -> bool: |
| | """Return True if request's HTTP BODY can be read, False otherwise.""" |
| | warnings.warn( |
| | "Deprecated, use .can_read_body #2005", DeprecationWarning, stacklevel=2 |
| | ) |
| | return not self._payload.at_eof() |
| |
|
| | @property |
| | def can_read_body(self) -> bool: |
| | """Return True if request's HTTP BODY can be read, False otherwise.""" |
| | return not self._payload.at_eof() |
| |
|
| | @reify |
| | def body_exists(self) -> bool: |
| | """Return True if request has HTTP BODY, False otherwise.""" |
| | return type(self._payload) is not EmptyStreamReader |
| |
|
| | async def release(self) -> None: |
| | """Release request. |
| | |
| | Eat unread part of HTTP BODY if present. |
| | """ |
| | while not self._payload.at_eof(): |
| | await self._payload.readany() |
| |
|
| | async def read(self) -> bytes: |
| | """Read request body if present. |
| | |
| | Returns bytes object with full request content. |
| | """ |
| | if self._read_bytes is None: |
| | body = bytearray() |
| | while True: |
| | chunk = await self._payload.readany() |
| | body.extend(chunk) |
| | if self._client_max_size: |
| | body_size = len(body) |
| | if body_size >= self._client_max_size: |
| | raise HTTPRequestEntityTooLarge( |
| | max_size=self._client_max_size, actual_size=body_size |
| | ) |
| | if not chunk: |
| | break |
| | self._read_bytes = bytes(body) |
| | return self._read_bytes |
| |
|
| | async def text(self) -> str: |
| | """Return BODY as text using encoding from .charset.""" |
| | bytes_body = await self.read() |
| | encoding = self.charset or "utf-8" |
| | return bytes_body.decode(encoding) |
| |
|
| | async def json(self, *, loads: JSONDecoder = DEFAULT_JSON_DECODER) -> Any: |
| | """Return BODY as JSON.""" |
| | body = await self.text() |
| | return loads(body) |
| |
|
| | async def multipart(self) -> MultipartReader: |
| | """Return async iterator to process BODY as multipart.""" |
| | return MultipartReader(self._headers, self._payload) |
| |
|
| | async def post(self) -> "MultiDictProxy[Union[str, bytes, FileField]]": |
| | """Return POST parameters.""" |
| | if self._post is not None: |
| | return self._post |
| | if self._method not in self.POST_METHODS: |
| | self._post = MultiDictProxy(MultiDict()) |
| | return self._post |
| |
|
| | content_type = self.content_type |
| | if content_type not in ( |
| | "", |
| | "application/x-www-form-urlencoded", |
| | "multipart/form-data", |
| | ): |
| | self._post = MultiDictProxy(MultiDict()) |
| | return self._post |
| |
|
| | out: MultiDict[Union[str, bytes, FileField]] = MultiDict() |
| |
|
| | if content_type == "multipart/form-data": |
| | multipart = await self.multipart() |
| | max_size = self._client_max_size |
| |
|
| | field = await multipart.next() |
| | while field is not None: |
| | size = 0 |
| | field_ct = field.headers.get(hdrs.CONTENT_TYPE) |
| |
|
| | if isinstance(field, BodyPartReader): |
| | assert field.name is not None |
| |
|
| | |
| | |
| | |
| | |
| | if field.filename: |
| | |
| | tmp = await self._loop.run_in_executor( |
| | None, tempfile.TemporaryFile |
| | ) |
| | chunk = await field.read_chunk(size=2**16) |
| | while chunk: |
| | chunk = field.decode(chunk) |
| | await self._loop.run_in_executor(None, tmp.write, chunk) |
| | size += len(chunk) |
| | if 0 < max_size < size: |
| | await self._loop.run_in_executor(None, tmp.close) |
| | raise HTTPRequestEntityTooLarge( |
| | max_size=max_size, actual_size=size |
| | ) |
| | chunk = await field.read_chunk(size=2**16) |
| | await self._loop.run_in_executor(None, tmp.seek, 0) |
| |
|
| | if field_ct is None: |
| | field_ct = "application/octet-stream" |
| |
|
| | ff = FileField( |
| | field.name, |
| | field.filename, |
| | cast(io.BufferedReader, tmp), |
| | field_ct, |
| | field.headers, |
| | ) |
| | out.add(field.name, ff) |
| | else: |
| | |
| | value = await field.read(decode=True) |
| | if field_ct is None or field_ct.startswith("text/"): |
| | charset = field.get_charset(default="utf-8") |
| | out.add(field.name, value.decode(charset)) |
| | else: |
| | out.add(field.name, value) |
| | size += len(value) |
| | if 0 < max_size < size: |
| | raise HTTPRequestEntityTooLarge( |
| | max_size=max_size, actual_size=size |
| | ) |
| | else: |
| | raise ValueError( |
| | "To decode nested multipart you need to use custom reader", |
| | ) |
| |
|
| | field = await multipart.next() |
| | else: |
| | data = await self.read() |
| | if data: |
| | charset = self.charset or "utf-8" |
| | out.extend( |
| | parse_qsl( |
| | data.rstrip().decode(charset), |
| | keep_blank_values=True, |
| | encoding=charset, |
| | ) |
| | ) |
| |
|
| | self._post = MultiDictProxy(out) |
| | return self._post |
| |
|
| | def get_extra_info(self, name: str, default: Any = None) -> Any: |
| | """Extra info from protocol transport""" |
| | protocol = self._protocol |
| | if protocol is None: |
| | return default |
| |
|
| | transport = protocol.transport |
| | if transport is None: |
| | return default |
| |
|
| | return transport.get_extra_info(name, default) |
| |
|
| | def __repr__(self) -> str: |
| | ascii_encodable_path = self.path.encode("ascii", "backslashreplace").decode( |
| | "ascii" |
| | ) |
| | return "<{} {} {} >".format( |
| | self.__class__.__name__, self._method, ascii_encodable_path |
| | ) |
| |
|
| | def __eq__(self, other: object) -> bool: |
| | return id(self) == id(other) |
| |
|
| | def __bool__(self) -> bool: |
| | return True |
| |
|
| | async def _prepare_hook(self, response: StreamResponse) -> None: |
| | return |
| |
|
| | def _cancel(self, exc: BaseException) -> None: |
| | set_exception(self._payload, exc) |
| |
|
| | def _finish(self) -> None: |
| | if self._post is None or self.content_type != "multipart/form-data": |
| | return |
| |
|
| | |
| | |
| | |
| | |
| | for file_name, file_field_object in self._post.items(): |
| | if isinstance(file_field_object, FileField): |
| | file_field_object.file.close() |
| |
|
| |
|
| | class Request(BaseRequest): |
| |
|
| | ATTRS = BaseRequest.ATTRS | frozenset(["_match_info"]) |
| |
|
| | _match_info: Optional["UrlMappingMatchInfo"] = None |
| |
|
| | if DEBUG: |
| |
|
| | def __setattr__(self, name: str, val: Any) -> None: |
| | if name not in self.ATTRS: |
| | warnings.warn( |
| | "Setting custom {}.{} attribute " |
| | "is discouraged".format(self.__class__.__name__, name), |
| | DeprecationWarning, |
| | stacklevel=2, |
| | ) |
| | super().__setattr__(name, val) |
| |
|
| | def clone( |
| | self, |
| | *, |
| | method: Union[str, _SENTINEL] = sentinel, |
| | rel_url: Union[StrOrURL, _SENTINEL] = sentinel, |
| | headers: Union[LooseHeaders, _SENTINEL] = sentinel, |
| | scheme: Union[str, _SENTINEL] = sentinel, |
| | host: Union[str, _SENTINEL] = sentinel, |
| | remote: Union[str, _SENTINEL] = sentinel, |
| | client_max_size: Union[int, _SENTINEL] = sentinel, |
| | ) -> "Request": |
| | ret = super().clone( |
| | method=method, |
| | rel_url=rel_url, |
| | headers=headers, |
| | scheme=scheme, |
| | host=host, |
| | remote=remote, |
| | client_max_size=client_max_size, |
| | ) |
| | new_ret = cast(Request, ret) |
| | new_ret._match_info = self._match_info |
| | return new_ret |
| |
|
| | @reify |
| | def match_info(self) -> "UrlMappingMatchInfo": |
| | """Result of route resolving.""" |
| | match_info = self._match_info |
| | assert match_info is not None |
| | return match_info |
| |
|
| | @property |
| | def app(self) -> "Application": |
| | """Application instance.""" |
| | match_info = self._match_info |
| | assert match_info is not None |
| | return match_info.current_app |
| |
|
| | @property |
| | def config_dict(self) -> ChainMapProxy: |
| | match_info = self._match_info |
| | assert match_info is not None |
| | lst = match_info.apps |
| | app = self.app |
| | idx = lst.index(app) |
| | sublist = list(reversed(lst[: idx + 1])) |
| | return ChainMapProxy(sublist) |
| |
|
| | async def _prepare_hook(self, response: StreamResponse) -> None: |
| | match_info = self._match_info |
| | if match_info is None: |
| | return |
| | for app in match_info._apps: |
| | if on_response_prepare := app.on_response_prepare: |
| | await on_response_prepare.send(self, response) |
| |
|