Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
web_request.py928 linesDownload Raw Back to aiohttp
1import asyncio
2import datetime
3import io
4import re
5import socket
6import string
7import tempfile
8import types
9import warnings
10from types import MappingProxyType
11from typing import (
12    TYPE_CHECKING,
13    Any,
14    Dict,
15    Final,
16    Iterator,
17    Mapping,
18    MutableMapping,
19    Optional,
20    Pattern,
21    Tuple,
22    Union,
23    cast,
24)
25from urllib.parse import parse_qsl
26
27import attr
28from multidict import (
29    CIMultiDict,
30    CIMultiDictProxy,
31    MultiDict,
32    MultiDictProxy,
33    MultiMapping,
34)
35from yarl import URL
36
37from . import hdrs
38from ._cookie_helpers import parse_cookie_header
39from .abc import AbstractStreamWriter
40from .helpers import (
41    _SENTINEL,
42    DEBUG,
43    ETAG_ANY,
44    LIST_QUOTED_ETAG_RE,
45    ChainMapProxy,
46    ETag,
47    HeadersMixin,
48    parse_http_date,
49    reify,
50    sentinel,
51    set_exception,
52)
53from .http_parser import RawRequestMessage
54from .http_writer import HttpVersion
55from .multipart import BodyPartReader, MultipartReader
56from .streams import EmptyStreamReader, StreamReader
57from .typedefs import (
58    DEFAULT_JSON_DECODER,
59    JSONDecoder,
60    LooseHeaders,
61    RawHeaders,
62    StrOrURL,
63)
64from .web_exceptions import HTTPRequestEntityTooLarge
65from .web_response import StreamResponse
66
67__all__ = ("BaseRequest", "FileField", "Request")
68
69
70if TYPE_CHECKING:
71    from .web_app import Application
72    from .web_protocol import RequestHandler
73    from .web_urldispatcher import UrlMappingMatchInfo
74
75
76@attr.s(auto_attribs=True, frozen=True, slots=True)
77class FileField:
78    name: str
79    filename: str
80    file: io.BufferedReader
81    content_type: str
82    headers: CIMultiDictProxy[str]
83
84
85_TCHAR: Final[str] = string.digits + string.ascii_letters + r"!#$%&'*+.^_`|~-"
86# '-' at the end to prevent interpretation as range in a char class
87
88_TOKEN: Final[str] = rf"[{_TCHAR}]+"
89
90_QDTEXT: Final[str] = r"[{}]".format(
91    r"".join(chr(c) for c in (0x09, 0x20, 0x21) + tuple(range(0x23, 0x7F)))
92)
93# qdtext includes 0x5C to escape 0x5D ('\]')
94# qdtext excludes obs-text (because obsoleted, and encoding not specified)
95
96_QUOTED_PAIR: Final[str] = r"\\[\t !-~]"
97
98_QUOTED_STRING: Final[str] = r'"(?:{quoted_pair}|{qdtext})*"'.format(
99    qdtext=_QDTEXT, quoted_pair=_QUOTED_PAIR
100)
101
102_FORWARDED_PAIR: Final[str] = (
103    r"({token})=({token}|{quoted_string})(:\d{{1,4}})?".format(
104        token=_TOKEN, quoted_string=_QUOTED_STRING
105    )
106)
107
108_QUOTED_PAIR_REPLACE_RE: Final[Pattern[str]] = re.compile(r"\\([\t !-~])")
109# same pattern as _QUOTED_PAIR but contains a capture group
110
111_FORWARDED_PAIR_RE: Final[Pattern[str]] = re.compile(_FORWARDED_PAIR)
112
113############################################################
114# HTTP Request
115############################################################
116
117
118class BaseRequest(MutableMapping[str, Any], HeadersMixin):
119
120    POST_METHODS = {
121        hdrs.METH_PATCH,
122        hdrs.METH_POST,
123        hdrs.METH_PUT,
124        hdrs.METH_TRACE,
125        hdrs.METH_DELETE,
126    }
127
128    ATTRS = HeadersMixin.ATTRS | frozenset(
129        [
130            "_message",
131            "_protocol",
132            "_payload_writer",
133            "_payload",
134            "_headers",
135            "_method",
136            "_version",
137            "_rel_url",
138            "_post",
139            "_read_bytes",
140            "_state",
141            "_cache",
142            "_task",
143            "_client_max_size",
144            "_loop",
145            "_transport_sslcontext",
146            "_transport_peername",
147        ]
148    )
149    _post: Optional[MultiDictProxy[Union[str, bytes, FileField]]] = None
150    _read_bytes: Optional[bytes] = None
151
152    def __init__(
153        self,
154        message: RawRequestMessage,
155        payload: StreamReader,
156        protocol: "RequestHandler",
157        payload_writer: AbstractStreamWriter,
158        task: "asyncio.Task[None]",
159        loop: asyncio.AbstractEventLoop,
160        *,
161        client_max_size: int = 1024**2,
162        state: Optional[Dict[str, Any]] = None,
163        scheme: Optional[str] = None,
164        host: Optional[str] = None,
165        remote: Optional[str] = None,
166    ) -> None:
167        self._message = message
168        self._protocol = protocol
169        self._payload_writer = payload_writer
170
171        self._payload = payload
172        self._headers: CIMultiDictProxy[str] = message.headers
173        self._method = message.method
174        self._version = message.version
175        self._cache: Dict[str, Any] = {}
176        url = message.url
177        if url.absolute:
178            if scheme is not None:
179                url = url.with_scheme(scheme)
180            if host is not None:
181                url = url.with_host(host)
182            # absolute URL is given,
183            # override auto-calculating url, host, and scheme
184            # all other properties should be good
185            self._cache["url"] = url
186            self._cache["host"] = url.host
187            self._cache["scheme"] = url.scheme
188            self._rel_url = url.relative()
189        else:
190            self._rel_url = url
191            if scheme is not None:
192                self._cache["scheme"] = scheme
193            if host is not None:
194                self._cache["host"] = host
195
196        self._state = {} if state is None else state
197        self._task = task
198        self._client_max_size = client_max_size
199        self._loop = loop
200
201        self._transport_sslcontext = protocol.ssl_context
202        self._transport_peername = protocol.peername
203
204        if remote is not None:
205            self._cache["remote"] = remote
206
207    def clone(
208        self,
209        *,
210        method: Union[str, _SENTINEL] = sentinel,
211        rel_url: Union[StrOrURL, _SENTINEL] = sentinel,
212        headers: Union[LooseHeaders, _SENTINEL] = sentinel,
213        scheme: Union[str, _SENTINEL] = sentinel,
214        host: Union[str, _SENTINEL] = sentinel,
215        remote: Union[str, _SENTINEL] = sentinel,
216        client_max_size: Union[int, _SENTINEL] = sentinel,
217    ) -> "BaseRequest":
218        """Clone itself with replacement some attributes.
219
220        Creates and returns a new instance of Request object. If no parameters
221        are given, an exact copy is returned. If a parameter is not passed, it
222        will reuse the one from the current request object.
223        """
224        if self._read_bytes:
225            raise RuntimeError("Cannot clone request after reading its content")
226
227        dct: Dict[str, Any] = {}
228        if method is not sentinel:
229            dct["method"] = method
230        if rel_url is not sentinel:
231            new_url: URL = URL(rel_url)
232            dct["url"] = new_url
233            dct["path"] = str(new_url)
234        if headers is not sentinel:
235            # a copy semantic
236            dct["headers"] = CIMultiDictProxy(CIMultiDict(headers))
237            dct["raw_headers"] = tuple(
238                (k.encode("utf-8"), v.encode("utf-8"))
239                for k, v in dct["headers"].items()
240            )
241
242        message = self._message._replace(**dct)
243
244        kwargs = {}
245        if scheme is not sentinel:
246            kwargs["scheme"] = scheme
247        if host is not sentinel:
248            kwargs["host"] = host
249        if remote is not sentinel:
250            kwargs["remote"] = remote
251        if client_max_size is sentinel:
252            client_max_size = self._client_max_size
253
254        return self.__class__(
255            message,
256            self._payload,
257            self._protocol,
258            self._payload_writer,
259            self._task,
260            self._loop,
261            client_max_size=client_max_size,
262            state=self._state.copy(),
263            **kwargs,
264        )
265
266    @property
267    def task(self) -> "asyncio.Task[None]":
268        return self._task
269
270    @property
271    def protocol(self) -> "RequestHandler":
272        return self._protocol
273
274    @property
275    def transport(self) -> Optional[asyncio.Transport]:
276        if self._protocol is None:
277            return None
278        return self._protocol.transport
279
280    @property
281    def writer(self) -> AbstractStreamWriter:
282        return self._payload_writer
283
284    @property
285    def client_max_size(self) -> int:
286        return self._client_max_size
287
288    @reify
289    def message(self) -> RawRequestMessage:
290        warnings.warn("Request.message is deprecated", DeprecationWarning, stacklevel=3)
291        return self._message
292
293    @reify
294    def rel_url(self) -> URL:
295        return self._rel_url
296
297    @reify
298    def loop(self) -> asyncio.AbstractEventLoop:
299        warnings.warn(
300            "request.loop property is deprecated", DeprecationWarning, stacklevel=2
301        )
302        return self._loop
303
304    # MutableMapping API
305
306    def __getitem__(self, key: str) -> Any:
307        return self._state[key]
308
309    def __setitem__(self, key: str, value: Any) -> None:
310        self._state[key] = value
311
312    def __delitem__(self, key: str) -> None:
313        del self._state[key]
314
315    def __len__(self) -> int:
316        return len(self._state)
317
318    def __iter__(self) -> Iterator[str]:
319        return iter(self._state)
320
321    ########
322
323    @reify
324    def secure(self) -> bool:
325        """A bool indicating if the request is handled with SSL."""
326        return self.scheme == "https"
327
328    @reify
329    def forwarded(self) -> Tuple[Mapping[str, str], ...]:
330        """A tuple containing all parsed Forwarded header(s).
331
332        Makes an effort to parse Forwarded headers as specified by RFC 7239:
333
334        - It adds one (immutable) dictionary per Forwarded 'field-value', ie
335          per proxy. The element corresponds to the data in the Forwarded
336          field-value added by the first proxy encountered by the client. Each
337          subsequent item corresponds to those added by later proxies.
338        - It checks that every value has valid syntax in general as specified
339          in section 4: either a 'token' or a 'quoted-string'.
340        - It un-escapes found escape sequences.
341        - It does NOT validate 'by' and 'for' contents as specified in section
342          6.
343        - It does NOT validate 'host' contents (Host ABNF).
344        - It does NOT validate 'proto' contents for valid URI scheme names.
345
346        Returns a tuple containing one or more immutable dicts
347        """
348        elems = []
349        for field_value in self._message.headers.getall(hdrs.FORWARDED, ()):
350            length = len(field_value)
351            pos = 0
352            need_separator = False
353            elem: Dict[str, str] = {}
354            elems.append(types.MappingProxyType(elem))
355            while 0 <= pos < length:
356                match = _FORWARDED_PAIR_RE.match(field_value, pos)
357                if match is not None:  # got a valid forwarded-pair
358                    if need_separator:
359                        # bad syntax here, skip to next comma
360                        pos = field_value.find(",", pos)
361                    else:
362                        name, value, port = match.groups()
363                        if value[0] == '"':
364                            # quoted string: remove quotes and unescape
365                            value = _QUOTED_PAIR_REPLACE_RE.sub(r"\1", value[1:-1])
366                        if port:
367                            value += port
368                        elem[name.lower()] = value
369                        pos += len(match.group(0))
370                        need_separator = True
371                elif field_value[pos] == ",":  # next forwarded-element
372                    need_separator = False
373                    elem = {}
374                    elems.append(types.MappingProxyType(elem))
375                    pos += 1
376                elif field_value[pos] == ";":  # next forwarded-pair
377                    need_separator = False
378                    pos += 1
379                elif field_value[pos] in " \t":
380                    # Allow whitespace even between forwarded-pairs, though
381                    # RFC 7239 doesn't. This simplifies code and is in line
382                    # with Postel's law.
383                    pos += 1
384                else:
385                    # bad syntax here, skip to next comma
386                    pos = field_value.find(",", pos)
387        return tuple(elems)
388
389    @reify
390    def scheme(self) -> str:
391        """A string representing the scheme of the request.
392
393        Hostname is resolved in this order:
394
395        - overridden value by .clone(scheme=new_scheme) call.
396        - type of connection to peer: HTTPS if socket is SSL, HTTP otherwise.
397
398        'http' or 'https'.
399        """
400        if self._transport_sslcontext:
401            return "https"
402        else:
403            return "http"
404
405    @reify
406    def method(self) -> str:
407        """Read only property for getting HTTP method.
408
409        The value is upper-cased str like 'GET', 'POST', 'PUT' etc.
410        """
411        return self._method
412
413    @reify
414    def version(self) -> HttpVersion:
415        """Read only property for getting HTTP version of request.
416
417        Returns aiohttp.protocol.HttpVersion instance.
418        """
419        return self._version
420
421    @reify
422    def host(self) -> str:
423        """Hostname of the request.
424
425        Hostname is resolved in this order:
426
427        - overridden value by .clone(host=new_host) call.
428        - HOST HTTP header
429        - socket.getfqdn() value
430
431        For example, 'example.com' or 'localhost:8080'.
432
433        For historical reasons, the port number may be included.
434        """
435        host = self._message.headers.get(hdrs.HOST)
436        if host is not None:
437            return host
438        return socket.getfqdn()
439
440    @reify
441    def remote(self) -> Optional[str]:
442        """Remote IP of client initiated HTTP request.
443
444        The IP is resolved in this order:
445
446        - overridden value by .clone(remote=new_remote) call.
447        - peername of opened socket
448        """
449        if self._transport_peername is None:
450            return None
451        if isinstance(self._transport_peername, (list, tuple)):
452            return str(self._transport_peername[0])
453        return str(self._transport_peername)
454
455    @reify
456    def url(self) -> URL:
457        """The full URL of the request."""
458        # authority is used here because it may include the port number
459        # and we want yarl to parse it correctly
460        return URL.build(scheme=self.scheme, authority=self.host).join(self._rel_url)
461
462    @reify
463    def path(self) -> str:
464        """The URL including *PATH INFO* without the host or scheme.
465
466        E.g., ``/app/blog``
467        """
468        return self._rel_url.path
469
470    @reify
471    def path_qs(self) -> str:
472        """The URL including PATH_INFO and the query string.
473
474        E.g, /app/blog?id=10
475        """
476        return str(self._rel_url)
477
478    @reify
479    def raw_path(self) -> str:
480        """The URL including raw *PATH INFO* without the host or scheme.
481
482        Warning, the path is unquoted and may contains non valid URL characters
483
484        E.g., ``/my%2Fpath%7Cwith%21some%25strange%24characters``
485        """
486        return self._message.path
487
488    @reify
489    def query(self) -> "MultiMapping[str]":
490        """A multidict with all the variables in the query string."""
491        return self._rel_url.query
492
493    @reify
494    def query_string(self) -> str:
495        """The query string in the URL.
496
497        E.g., id=10
498        """
499        return self._rel_url.query_string
500
501    @reify
502    def headers(self) -> CIMultiDictProxy[str]:
503        """A case-insensitive multidict proxy with all headers."""
504        return self._headers
505
506    @reify
507    def raw_headers(self) -> RawHeaders:
508        """A sequence of pairs for all headers."""
509        return self._message.raw_headers
510
511    @reify
512    def if_modified_since(self) -> Optional[datetime.datetime]:
513        """The value of If-Modified-Since HTTP header, or None.
514
515        This header is represented as a `datetime` object.
516        """
517        return parse_http_date(self.headers.get(hdrs.IF_MODIFIED_SINCE))
518
519    @reify
520    def if_unmodified_since(self) -> Optional[datetime.datetime]:
521        """The value of If-Unmodified-Since HTTP header, or None.
522
523        This header is represented as a `datetime` object.
524        """
525        return parse_http_date(self.headers.get(hdrs.IF_UNMODIFIED_SINCE))
526
527    @staticmethod
528    def _etag_values(etag_header: str) -> Iterator[ETag]:
529        """Extract `ETag` objects from raw header."""
530        if etag_header == ETAG_ANY:
531            yield ETag(
532                is_weak=False,
533                value=ETAG_ANY,
534            )
535        else:
536            for match in LIST_QUOTED_ETAG_RE.finditer(etag_header):
537                is_weak, value, garbage = match.group(2, 3, 4)
538                # Any symbol captured by 4th group means
539                # that the following sequence is invalid.
540                if garbage:
541                    break
542
543                yield ETag(
544                    is_weak=bool(is_weak),
545                    value=value,
546                )
547
548    @classmethod
549    def _if_match_or_none_impl(
550        cls, header_value: Optional[str]
551    ) -> Optional[Tuple[ETag, ...]]:
552        if not header_value:
553            return None
554
555        return tuple(cls._etag_values(header_value))
556
557    @reify
558    def if_match(self) -> Optional[Tuple[ETag, ...]]:
559        """The value of If-Match HTTP header, or None.
560
561        This header is represented as a `tuple` of `ETag` objects.
562        """
563        return self._if_match_or_none_impl(self.headers.get(hdrs.IF_MATCH))
564
565    @reify
566    def if_none_match(self) -> Optional[Tuple[ETag, ...]]:
567        """The value of If-None-Match HTTP header, or None.
568
569        This header is represented as a `tuple` of `ETag` objects.
570        """
571        return self._if_match_or_none_impl(self.headers.get(hdrs.IF_NONE_MATCH))
572
573    @reify
574    def if_range(self) -> Optional[datetime.datetime]:
575        """The value of If-Range HTTP header, or None.
576
577        This header is represented as a `datetime` object.
578        """
579        return parse_http_date(self.headers.get(hdrs.IF_RANGE))
580
581    @reify
582    def keep_alive(self) -> bool:
583        """Is keepalive enabled by client?"""
584        return not self._message.should_close
585
586    @reify
587    def cookies(self) -> Mapping[str, str]:
588        """Return request cookies.
589
590        A read-only dictionary-like object.
591        """
592        # Use parse_cookie_header for RFC 6265 compliant Cookie header parsing
593        # that accepts special characters in cookie names (fixes #2683)
594        parsed = parse_cookie_header(self.headers.get(hdrs.COOKIE, ""))
595        # Extract values from Morsel objects
596        return MappingProxyType({name: morsel.value for name, morsel in parsed})
597
598    @reify
599    def http_range(self) -> slice:
600        """The content of Range HTTP header.
601
602        Return a slice instance.
603
604        """
605        rng = self._headers.get(hdrs.RANGE)
606        start, end = None, None
607        if rng is not None:
608            try:
609                pattern = r"^bytes=(\d*)-(\d*)$"
610                start, end = re.findall(pattern, rng, re.ASCII)[0]
611            except IndexError:  # pattern was not found in header
612                raise ValueError("range not in acceptable format")
613
614            end = int(end) if end else None
615            start = int(start) if start else None
616
617            if start is None and end is not None:
618                # end with no start is to return tail of content
619                start = -end
620                end = None
621
622            if start is not None and end is not None:
623                # end is inclusive in range header, exclusive for slice
624                end += 1
625
626                if start >= end:
627                    raise ValueError("start cannot be after end")
628
629            if start is end is None:  # No valid range supplied
630                raise ValueError("No start or end of range specified")
631
632        return slice(start, end, 1)
633
634    @reify
635    def content(self) -> StreamReader:
636        """Return raw payload stream."""
637        return self._payload
638
639    @property
640    def has_body(self) -> bool:
641        """Return True if request's HTTP BODY can be read, False otherwise."""
642        warnings.warn(
643            "Deprecated, use .can_read_body #2005", DeprecationWarning, stacklevel=2
644        )
645        return not self._payload.at_eof()
646
647    @property
648    def can_read_body(self) -> bool:
649        """Return True if request's HTTP BODY can be read, False otherwise."""
650        return not self._payload.at_eof()
651
652    @reify
653    def body_exists(self) -> bool:
654        """Return True if request has HTTP BODY, False otherwise."""
655        return type(self._payload) is not EmptyStreamReader
656
657    async def release(self) -> None:
658        """Release request.
659
660        Eat unread part of HTTP BODY if present.
661        """
662        while not self._payload.at_eof():
663            await self._payload.readany()
664
665    async def read(self) -> bytes:
666        """Read request body if present.
667
668        Returns bytes object with full request content.
669        """
670        if self._read_bytes is None:
671            body = bytearray()
672            while True:
673                chunk = await self._payload.readany()
674                body.extend(chunk)
675                if self._client_max_size:
676                    body_size = len(body)
677                    if body_size >= self._client_max_size:
678                        raise HTTPRequestEntityTooLarge(
679                            max_size=self._client_max_size, actual_size=body_size
680                        )
681                if not chunk:
682                    break
683            self._read_bytes = bytes(body)
684        return self._read_bytes
685
686    async def text(self) -> str:
687        """Return BODY as text using encoding from .charset."""
688        bytes_body = await self.read()
689        encoding = self.charset or "utf-8"
690        return bytes_body.decode(encoding)
691
692    async def json(self, *, loads: JSONDecoder = DEFAULT_JSON_DECODER) -> Any:
693        """Return BODY as JSON."""
694        body = await self.text()
695        return loads(body)
696
697    async def multipart(self) -> MultipartReader:
698        """Return async iterator to process BODY as multipart."""
699        return MultipartReader(
700            self._headers,
701            self._payload,
702            max_field_size=self._protocol.max_field_size,
703            max_headers=self._protocol.max_headers,
704        )
705
706    async def post(self) -> "MultiDictProxy[Union[str, bytes, FileField]]":
707        """Return POST parameters."""
708        if self._post is not None:
709            return self._post
710        if self._method not in self.POST_METHODS:
711            self._post = MultiDictProxy(MultiDict())
712            return self._post
713
714        content_type = self.content_type
715        if content_type not in (
716            "",
717            "application/x-www-form-urlencoded",
718            "multipart/form-data",
719        ):
720            self._post = MultiDictProxy(MultiDict())
721            return self._post
722
723        out: MultiDict[Union[str, bytes, FileField]] = MultiDict()
724
725        if content_type == "multipart/form-data":
726            multipart = await self.multipart()
727            max_size = self._client_max_size
728
729            size = 0
730            while (field := await multipart.next()) is not None:
731                field_ct = field.headers.get(hdrs.CONTENT_TYPE)
732
733                if isinstance(field, BodyPartReader):
734                    if field.name is None:
735                        raise ValueError("Multipart field missing name.")
736
737                    # Note that according to RFC 7578, the Content-Type header
738                    # is optional, even for files, so we can't assume it's
739                    # present.
740                    # https://tools.ietf.org/html/rfc7578#section-4.4
741                    if field.filename:
742                        # store file in temp file
743                        tmp = await self._loop.run_in_executor(
744                            None, tempfile.TemporaryFile
745                        )
746                        while chunk := await field.read_chunk(size=2**18):
747                            async for decoded_chunk in field.decode_iter(chunk):
748                                await self._loop.run_in_executor(
749                                    None, tmp.write, decoded_chunk
750                                )
751                                size += len(decoded_chunk)
752                                if 0 < max_size < size:
753                                    await self._loop.run_in_executor(None, tmp.close)
754                                    raise HTTPRequestEntityTooLarge(
755                                        max_size=max_size, actual_size=size
756                                    )
757                        await self._loop.run_in_executor(None, tmp.seek, 0)
758
759                        if field_ct is None:
760                            field_ct = "application/octet-stream"
761
762                        ff = FileField(
763                            field.name,
764                            field.filename,
765                            cast(io.BufferedReader, tmp),
766                            field_ct,
767                            field.headers,
768                        )
769                        out.add(field.name, ff)
770                    else:
771                        # deal with ordinary data
772                        raw_data = bytearray()
773                        while chunk := await field.read_chunk():
774                            size += len(chunk)
775                            if 0 < max_size < size:
776                                raise HTTPRequestEntityTooLarge(
777                                    max_size=max_size, actual_size=size
778                                )
779                            raw_data.extend(chunk)
780
781                        value = bytearray()
782                        # form-data doesn't support compression, so don't need to check size again.
783                        async for d in field.decode_iter(raw_data):
784                            value.extend(d)
785
786                        if field_ct is None or field_ct.startswith("text/"):
787                            charset = field.get_charset(default="utf-8")
788                            out.add(field.name, value.decode(charset))
789                        else:
790                            out.add(field.name, value)
791                else:
792                    raise ValueError(
793                        "To decode nested multipart you need to use custom reader",
794                    )
795        else:
796            data = await self.read()
797            if data:
798                charset = self.charset or "utf-8"
799                out.extend(
800                    parse_qsl(
801                        data.rstrip().decode(charset),
802                        keep_blank_values=True,
803                        encoding=charset,
804                    )
805                )
806
807        self._post = MultiDictProxy(out)
808        return self._post
809
810    def get_extra_info(self, name: str, default: Any = None) -> Any:
811        """Extra info from protocol transport"""
812        protocol = self._protocol
813        if protocol is None:
814            return default
815
816        transport = protocol.transport
817        if transport is None:
818            return default
819
820        return transport.get_extra_info(name, default)
821
822    def __repr__(self) -> str:
823        ascii_encodable_path = self.path.encode("ascii", "backslashreplace").decode(
824            "ascii"
825        )
826        return "<{} {} {} >".format(
827            self.__class__.__name__, self._method, ascii_encodable_path
828        )
829
830    def __eq__(self, other: object) -> bool:
831        return id(self) == id(other)
832
833    def __bool__(self) -> bool:
834        return True
835
836    async def _prepare_hook(self, response: StreamResponse) -> None:
837        return
838
839    def _cancel(self, exc: BaseException) -> None:
840        set_exception(self._payload, exc)
841
842    def _finish(self) -> None:
843        if self._post is None or self.content_type != "multipart/form-data":
844            return
845
846        # NOTE: Release file descriptors for the
847        # NOTE: `tempfile.Temporaryfile`-created `_io.BufferedRandom`
848        # NOTE: instances of files sent within multipart request body
849        # NOTE: via HTTP POST request.
850        for file_name, file_field_object in self._post.items():
851            if isinstance(file_field_object, FileField):
852                file_field_object.file.close()
853
854
855class Request(BaseRequest):
856
857    ATTRS = BaseRequest.ATTRS | frozenset(["_match_info"])
858
859    _match_info: Optional["UrlMappingMatchInfo"] = None
860
861    if DEBUG:
862
863        def __setattr__(self, name: str, val: Any) -> None:
864            if name not in self.ATTRS:
865                warnings.warn(
866                    "Setting custom {}.{} attribute "
867                    "is discouraged".format(self.__class__.__name__, name),
868                    DeprecationWarning,
869                    stacklevel=2,
870                )
871            super().__setattr__(name, val)
872
873    def clone(
874        self,
875        *,
876        method: Union[str, _SENTINEL] = sentinel,
877        rel_url: Union[StrOrURL, _SENTINEL] = sentinel,
878        headers: Union[LooseHeaders, _SENTINEL] = sentinel,
879        scheme: Union[str, _SENTINEL] = sentinel,
880        host: Union[str, _SENTINEL] = sentinel,
881        remote: Union[str, _SENTINEL] = sentinel,
882        client_max_size: Union[int, _SENTINEL] = sentinel,
883    ) -> "Request":
884        ret = super().clone(
885            method=method,
886            rel_url=rel_url,
887            headers=headers,
888            scheme=scheme,
889            host=host,
890            remote=remote,
891            client_max_size=client_max_size,
892        )
893        new_ret = cast(Request, ret)
894        new_ret._match_info = self._match_info
895        return new_ret
896
897    @reify
898    def match_info(self) -> "UrlMappingMatchInfo":
899        """Result of route resolving."""
900        match_info = self._match_info
901        assert match_info is not None
902        return match_info
903
904    @property
905    def app(self) -> "Application":
906        """Application instance."""
907        match_info = self._match_info
908        assert match_info is not None
909        return match_info.current_app
910
911    @property
912    def config_dict(self) -> ChainMapProxy:
913        match_info = self._match_info
914        assert match_info is not None
915        lst = match_info.apps
916        app = self.app
917        idx = lst.index(app)
918        sublist = list(reversed(lst[: idx + 1]))
919        return ChainMapProxy(sublist)
920
921    async def _prepare_hook(self, response: StreamResponse) -> None:
922        match_info = self._match_info
923        if match_info is None:
924            return
925        for app in match_info._apps:
926            if on_response_prepare := app.on_response_prepare:
927                await on_response_prepare.send(self, response)
928 
codekingpro/portable-devtools · Team Ai