Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
client_reqrep.py1537 linesDownload Raw Back to aiohttp
1import asyncio
2import codecs
3import contextlib
4import functools
5import io
6import re
7import sys
8import traceback
9import warnings
10from collections.abc import Mapping
11from hashlib import md5, sha1, sha256
12from http.cookies import Morsel, SimpleCookie
13from types import MappingProxyType, TracebackType
14from typing import (
15    TYPE_CHECKING,
16    Any,
17    Callable,
18    Dict,
19    Iterable,
20    List,
21    Literal,
22    NamedTuple,
23    Optional,
24    Tuple,
25    Type,
26    Union,
27)
28
29import attr
30from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy
31from yarl import URL
32
33from . import hdrs, helpers, http, multipart, payload
34from ._cookie_helpers import (
35    parse_cookie_header,
36    parse_set_cookie_headers,
37    preserve_morsel_with_coded_value,
38)
39from .abc import AbstractStreamWriter
40from .client_exceptions import (
41    ClientConnectionError,
42    ClientOSError,
43    ClientResponseError,
44    ContentTypeError,
45    InvalidURL,
46    ServerFingerprintMismatch,
47)
48from .compression_utils import HAS_BROTLI, HAS_ZSTD
49from .formdata import FormData
50from .helpers import (
51    _SENTINEL,
52    BaseTimerContext,
53    BasicAuth,
54    HeadersMixin,
55    TimerNoop,
56    noop,
57    reify,
58    sentinel,
59    set_exception,
60    set_result,
61)
62from .http import (
63    SERVER_SOFTWARE,
64    HttpVersion,
65    HttpVersion10,
66    HttpVersion11,
67    StreamWriter,
68)
69from .streams import StreamReader
70from .typedefs import (
71    DEFAULT_JSON_DECODER,
72    JSONDecoder,
73    LooseCookies,
74    LooseHeaders,
75    Query,
76    RawHeaders,
77)
78
79if TYPE_CHECKING:
80    import ssl
81    from ssl import SSLContext
82else:
83    try:
84        import ssl
85        from ssl import SSLContext
86    except ImportError:  # pragma: no cover
87        ssl = None  # type: ignore[assignment]
88        SSLContext = object  # type: ignore[misc,assignment]
89
90
91__all__ = ("ClientRequest", "ClientResponse", "RequestInfo", "Fingerprint")
92
93
94if TYPE_CHECKING:
95    from .client import ClientSession
96    from .connector import Connection
97    from .tracing import Trace
98
99
100_CONNECTION_CLOSED_EXCEPTION = ClientConnectionError("Connection closed")
101_CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]")
102json_re = re.compile(r"^application/(?:[\w.+-]+?\+)?json")
103
104
105def _gen_default_accept_encoding() -> str:
106    encodings = [
107        "gzip",
108        "deflate",
109    ]
110    if HAS_BROTLI:
111        encodings.append("br")
112    if HAS_ZSTD:
113        encodings.append("zstd")
114    return ", ".join(encodings)
115
116
117@attr.s(auto_attribs=True, frozen=True, slots=True)
118class ContentDisposition:
119    type: Optional[str]
120    parameters: "MappingProxyType[str, str]"
121    filename: Optional[str]
122
123
124class _RequestInfo(NamedTuple):
125    url: URL
126    method: str
127    headers: "CIMultiDictProxy[str]"
128    real_url: URL
129
130
131class RequestInfo(_RequestInfo):
132
133    def __new__(
134        cls,
135        url: URL,
136        method: str,
137        headers: "CIMultiDictProxy[str]",
138        real_url: Union[URL, _SENTINEL] = sentinel,
139    ) -> "RequestInfo":
140        """Create a new RequestInfo instance.
141
142        For backwards compatibility, the real_url parameter is optional.
143        """
144        return tuple.__new__(
145            cls, (url, method, headers, url if real_url is sentinel else real_url)
146        )
147
148
149class Fingerprint:
150    HASHFUNC_BY_DIGESTLEN = {
151        16: md5,
152        20: sha1,
153        32: sha256,
154    }
155
156    def __init__(self, fingerprint: bytes) -> None:
157        digestlen = len(fingerprint)
158        hashfunc = self.HASHFUNC_BY_DIGESTLEN.get(digestlen)
159        if not hashfunc:
160            raise ValueError("fingerprint has invalid length")
161        elif hashfunc is md5 or hashfunc is sha1:
162            raise ValueError("md5 and sha1 are insecure and not supported. Use sha256.")
163        self._hashfunc = hashfunc
164        self._fingerprint = fingerprint
165
166    @property
167    def fingerprint(self) -> bytes:
168        return self._fingerprint
169
170    def check(self, transport: asyncio.Transport) -> None:
171        if not transport.get_extra_info("sslcontext"):
172            return
173        sslobj = transport.get_extra_info("ssl_object")
174        cert = sslobj.getpeercert(binary_form=True)
175        got = self._hashfunc(cert).digest()
176        if got != self._fingerprint:
177            host, port, *_ = transport.get_extra_info("peername")
178            raise ServerFingerprintMismatch(self._fingerprint, got, host, port)
179
180
181if ssl is not None:
182    SSL_ALLOWED_TYPES = (ssl.SSLContext, bool, Fingerprint, type(None))
183else:  # pragma: no cover
184    SSL_ALLOWED_TYPES = (bool, type(None))
185
186
187def _merge_ssl_params(
188    ssl: Union["SSLContext", bool, Fingerprint],
189    verify_ssl: Optional[bool],
190    ssl_context: Optional["SSLContext"],
191    fingerprint: Optional[bytes],
192) -> Union["SSLContext", bool, Fingerprint]:
193    if ssl is None:
194        ssl = True  # Double check for backwards compatibility
195    if verify_ssl is not None and not verify_ssl:
196        warnings.warn(
197            "verify_ssl is deprecated, use ssl=False instead",
198            DeprecationWarning,
199            stacklevel=3,
200        )
201        if ssl is not True:
202            raise ValueError(
203                "verify_ssl, ssl_context, fingerprint and ssl "
204                "parameters are mutually exclusive"
205            )
206        else:
207            ssl = False
208    if ssl_context is not None:
209        warnings.warn(
210            "ssl_context is deprecated, use ssl=context instead",
211            DeprecationWarning,
212            stacklevel=3,
213        )
214        if ssl is not True:
215            raise ValueError(
216                "verify_ssl, ssl_context, fingerprint and ssl "
217                "parameters are mutually exclusive"
218            )
219        else:
220            ssl = ssl_context
221    if fingerprint is not None:
222        warnings.warn(
223            "fingerprint is deprecated, use ssl=Fingerprint(fingerprint) instead",
224            DeprecationWarning,
225            stacklevel=3,
226        )
227        if ssl is not True:
228            raise ValueError(
229                "verify_ssl, ssl_context, fingerprint and ssl "
230                "parameters are mutually exclusive"
231            )
232        else:
233            ssl = Fingerprint(fingerprint)
234    if not isinstance(ssl, SSL_ALLOWED_TYPES):
235        raise TypeError(
236            "ssl should be SSLContext, bool, Fingerprint or None, "
237            "got {!r} instead.".format(ssl)
238        )
239    return ssl
240
241
242_SSL_SCHEMES = frozenset(("https", "wss"))
243
244
245# ConnectionKey is a NamedTuple because it is used as a key in a dict
246# and a set in the connector. Since a NamedTuple is a tuple it uses
247# the fast native tuple __hash__ and __eq__ implementation in CPython.
248class ConnectionKey(NamedTuple):
249    # the key should contain an information about used proxy / TLS
250    # to prevent reusing wrong connections from a pool
251    host: str
252    port: Optional[int]
253    is_ssl: bool
254    ssl: Union[SSLContext, bool, Fingerprint]
255    proxy: Optional[URL]
256    proxy_auth: Optional[BasicAuth]
257    proxy_headers_hash: Optional[int]  # hash(CIMultiDict)
258
259
260def _is_expected_content_type(
261    response_content_type: str, expected_content_type: str
262) -> bool:
263    if expected_content_type == "application/json":
264        return json_re.match(response_content_type) is not None
265    return expected_content_type in response_content_type
266
267
268def _warn_if_unclosed_payload(payload: payload.Payload, stacklevel: int = 2) -> None:
269    """Warn if the payload is not closed.
270
271    Callers must check that the body is a Payload before calling this method.
272
273    Args:
274        payload: The payload to check
275        stacklevel: Stack level for the warning (default 2 for direct callers)
276    """
277    if not payload.autoclose and not payload.consumed:
278        warnings.warn(
279            "The previous request body contains unclosed resources. "
280            "Use await request.update_body() instead of setting request.body "
281            "directly to properly close resources and avoid leaks.",
282            ResourceWarning,
283            stacklevel=stacklevel,
284        )
285
286
287class ClientResponse(HeadersMixin):
288
289    # Some of these attributes are None when created,
290    # but will be set by the start() method.
291    # As the end user will likely never see the None values, we cheat the types below.
292    # from the Status-Line of the response
293    version: Optional[HttpVersion] = None  # HTTP-Version
294    status: int = None  # type: ignore[assignment] # Status-Code
295    reason: Optional[str] = None  # Reason-Phrase
296
297    content: StreamReader = None  # type: ignore[assignment] # Payload stream
298    _body: Optional[bytes] = None
299    _headers: CIMultiDictProxy[str] = None  # type: ignore[assignment]
300    _history: Tuple["ClientResponse", ...] = ()
301    _raw_headers: RawHeaders = None  # type: ignore[assignment]
302
303    _connection: Optional["Connection"] = None  # current connection
304    _cookies: Optional[SimpleCookie] = None
305    _raw_cookie_headers: Optional[Tuple[str, ...]] = None
306    _continue: Optional["asyncio.Future[bool]"] = None
307    _source_traceback: Optional[traceback.StackSummary] = None
308    _session: Optional["ClientSession"] = None
309    # set up by ClientRequest after ClientResponse object creation
310    # post-init stage allows to not change ctor signature
311    _closed = True  # to allow __del__ for non-initialized properly response
312    _released = False
313    _in_context = False
314
315    _resolve_charset: Callable[["ClientResponse", bytes], str] = lambda *_: "utf-8"
316
317    __writer: Optional["asyncio.Task[None]"] = None
318
319    def __init__(
320        self,
321        method: str,
322        url: URL,
323        *,
324        writer: "Optional[asyncio.Task[None]]",
325        continue100: Optional["asyncio.Future[bool]"],
326        timer: BaseTimerContext,
327        request_info: RequestInfo,
328        traces: List["Trace"],
329        loop: asyncio.AbstractEventLoop,
330        session: "ClientSession",
331    ) -> None:
332        # URL forbids subclasses, so a simple type check is enough.
333        assert type(url) is URL
334
335        self.method = method
336
337        self._real_url = url
338        self._url = url.with_fragment(None) if url.raw_fragment else url
339        if writer is not None:
340            self._writer = writer
341        if continue100 is not None:
342            self._continue = continue100
343        self._request_info = request_info
344        self._timer = timer if timer is not None else TimerNoop()
345        self._cache: Dict[str, Any] = {}
346        self._traces = traces
347        self._loop = loop
348        # Save reference to _resolve_charset, so that get_encoding() will still
349        # work after the response has finished reading the body.
350        # TODO: Fix session=None in tests (see ClientRequest.__init__).
351        if session is not None:
352            # store a reference to session #1985
353            self._session = session
354            self._resolve_charset = session._resolve_charset
355        if loop.get_debug():
356            self._source_traceback = traceback.extract_stack(sys._getframe(1))
357
358    def __reset_writer(self, _: object = None) -> None:
359        self.__writer = None
360
361    @property
362    def _writer(self) -> Optional["asyncio.Task[None]"]:
363        """The writer task for streaming data.
364
365        _writer is only provided for backwards compatibility
366        for subclasses that may need to access it.
367        """
368        return self.__writer
369
370    @_writer.setter
371    def _writer(self, writer: Optional["asyncio.Task[None]"]) -> None:
372        """Set the writer task for streaming data."""
373        if self.__writer is not None:
374            self.__writer.remove_done_callback(self.__reset_writer)
375        self.__writer = writer
376        if writer is None:
377            return
378        if writer.done():
379            # The writer is already done, so we can clear it immediately.
380            self.__writer = None
381        else:
382            writer.add_done_callback(self.__reset_writer)
383
384    @property
385    def cookies(self) -> SimpleCookie:
386        if self._cookies is None:
387            if self._raw_cookie_headers is not None:
388                # Parse cookies for response.cookies (SimpleCookie for backward compatibility)
389                cookies = SimpleCookie()
390                # Use parse_set_cookie_headers for more lenient parsing that handles
391                # malformed cookies better than SimpleCookie.load
392                cookies.update(parse_set_cookie_headers(self._raw_cookie_headers))
393                self._cookies = cookies
394            else:
395                self._cookies = SimpleCookie()
396        return self._cookies
397
398    @cookies.setter
399    def cookies(self, cookies: SimpleCookie) -> None:
400        self._cookies = cookies
401        # Generate raw cookie headers from the SimpleCookie
402        if cookies:
403            self._raw_cookie_headers = tuple(
404                morsel.OutputString() for morsel in cookies.values()
405            )
406        else:
407            self._raw_cookie_headers = None
408
409    @reify
410    def url(self) -> URL:
411        return self._url
412
413    @reify
414    def url_obj(self) -> URL:
415        warnings.warn("Deprecated, use .url #1654", DeprecationWarning, stacklevel=2)
416        return self._url
417
418    @reify
419    def real_url(self) -> URL:
420        return self._real_url
421
422    @reify
423    def host(self) -> str:
424        assert self._url.host is not None
425        return self._url.host
426
427    @reify
428    def headers(self) -> "CIMultiDictProxy[str]":
429        return self._headers
430
431    @reify
432    def raw_headers(self) -> RawHeaders:
433        return self._raw_headers
434
435    @reify
436    def request_info(self) -> RequestInfo:
437        return self._request_info
438
439    @reify
440    def content_disposition(self) -> Optional[ContentDisposition]:
441        raw = self._headers.get(hdrs.CONTENT_DISPOSITION)
442        if raw is None:
443            return None
444        disposition_type, params_dct = multipart.parse_content_disposition(raw)
445        params = MappingProxyType(params_dct)
446        filename = multipart.content_disposition_filename(params)
447        return ContentDisposition(disposition_type, params, filename)
448
449    def __del__(self, _warnings: Any = warnings) -> None:
450        if self._closed:
451            return
452
453        if self._connection is not None:
454            self._connection.release()
455            self._cleanup_writer()
456
457            if self._loop.get_debug():
458                kwargs = {"source": self}
459                _warnings.warn(f"Unclosed response {self!r}", ResourceWarning, **kwargs)
460                context = {"client_response": self, "message": "Unclosed response"}
461                if self._source_traceback:
462                    context["source_traceback"] = self._source_traceback
463                self._loop.call_exception_handler(context)
464
465    def __repr__(self) -> str:
466        out = io.StringIO()
467        ascii_encodable_url = str(self.url)
468        if self.reason:
469            ascii_encodable_reason = self.reason.encode(
470                "ascii", "backslashreplace"
471            ).decode("ascii")
472        else:
473            ascii_encodable_reason = "None"
474        print(
475            "<ClientResponse({}) [{} {}]>".format(
476                ascii_encodable_url, self.status, ascii_encodable_reason
477            ),
478            file=out,
479        )
480        print(self.headers, file=out)
481        return out.getvalue()
482
483    @property
484    def connection(self) -> Optional["Connection"]:
485        return self._connection
486
487    @reify
488    def history(self) -> Tuple["ClientResponse", ...]:
489        """A sequence of of responses, if redirects occurred."""
490        return self._history
491
492    @reify
493    def links(self) -> "MultiDictProxy[MultiDictProxy[Union[str, URL]]]":
494        links_str = ", ".join(self.headers.getall("link", []))
495
496        if not links_str:
497            return MultiDictProxy(MultiDict())
498
499        links: MultiDict[MultiDictProxy[Union[str, URL]]] = MultiDict()
500
501        for val in re.split(r",(?=\s*<)", links_str):
502            match = re.match(r"\s*<(.*)>(.*)", val)
503            if match is None:  # pragma: no cover
504                # the check exists to suppress mypy error
505                continue
506            url, params_str = match.groups()
507            params = params_str.split(";")[1:]
508
509            link: MultiDict[Union[str, URL]] = MultiDict()
510
511            for param in params:
512                match = re.match(r"^\s*(\S*)\s*=\s*(['\"]?)(.*?)(\2)\s*$", param, re.M)
513                if match is None:  # pragma: no cover
514                    # the check exists to suppress mypy error
515                    continue
516                key, _, value, _ = match.groups()
517
518                link.add(key, value)
519
520            key = link.get("rel", url)
521
522            link.add("url", self.url.join(URL(url)))
523
524            links.add(str(key), MultiDictProxy(link))
525
526        return MultiDictProxy(links)
527
528    async def start(self, connection: "Connection") -> "ClientResponse":
529        """Start response processing."""
530        self._closed = False
531        self._protocol = connection.protocol
532        self._connection = connection
533
534        with self._timer:
535            while True:
536                # read response
537                try:
538                    protocol = self._protocol
539                    message, payload = await protocol.read()  # type: ignore[union-attr]
540                except http.HttpProcessingError as exc:
541                    raise ClientResponseError(
542                        self.request_info,
543                        self.history,
544                        status=exc.code,
545                        message=exc.message,
546                        headers=exc.headers,
547                    ) from exc
548
549                if message.code < 100 or message.code > 199 or message.code == 101:
550                    break
551
552                if self._continue is not None:
553                    set_result(self._continue, True)
554                    self._continue = None
555
556        # payload eof handler
557        payload.on_eof(self._response_eof)
558
559        # response status
560        self.version = message.version
561        self.status = message.code
562        self.reason = message.reason
563
564        # headers
565        self._headers = message.headers  # type is CIMultiDictProxy
566        self._raw_headers = message.raw_headers  # type is Tuple[bytes, bytes]
567
568        # payload
569        self.content = payload
570
571        # cookies
572        if cookie_hdrs := self.headers.getall(hdrs.SET_COOKIE, ()):
573            # Store raw cookie headers for CookieJar
574            self._raw_cookie_headers = tuple(cookie_hdrs)
575        return self
576
577    def _response_eof(self) -> None:
578        if self._closed:
579            return
580
581        # protocol could be None because connection could be detached
582        protocol = self._connection and self._connection.protocol
583        if protocol is not None and protocol.upgraded:
584            return
585
586        self._closed = True
587        self._cleanup_writer()
588        self._release_connection()
589
590    @property
591    def closed(self) -> bool:
592        return self._closed
593
594    def close(self) -> None:
595        if not self._released:
596            self._notify_content()
597
598        self._closed = True
599        if self._loop is None or self._loop.is_closed():
600            return
601
602        self._cleanup_writer()
603        if self._connection is not None:
604            self._connection.close()
605            self._connection = None
606
607    def release(self) -> Any:
608        if not self._released:
609            self._notify_content()
610
611        self._closed = True
612
613        self._cleanup_writer()
614        self._release_connection()
615        return noop()
616
617    @property
618    def ok(self) -> bool:
619        """Returns ``True`` if ``status`` is less than ``400``, ``False`` if not.
620
621        This is **not** a check for ``200 OK`` but a check that the response
622        status is under 400.
623        """
624        return 400 > self.status
625
626    def raise_for_status(self) -> None:
627        if not self.ok:
628            # reason should always be not None for a started response
629            assert self.reason is not None
630
631            # If we're in a context we can rely on __aexit__() to release as the
632            # exception propagates.
633            if not self._in_context:
634                self.release()
635
636            raise ClientResponseError(
637                self.request_info,
638                self.history,
639                status=self.status,
640                message=self.reason,
641                headers=self.headers,
642            )
643
644    def _release_connection(self) -> None:
645        if self._connection is not None:
646            if self.__writer is None:
647                self._connection.release()
648                self._connection = None
649            else:
650                self.__writer.add_done_callback(lambda f: self._release_connection())
651
652    async def _wait_released(self) -> None:
653        if self.__writer is not None:
654            try:
655                await self.__writer
656            except asyncio.CancelledError:
657                if (
658                    sys.version_info >= (3, 11)
659                    and (task := asyncio.current_task())
660                    and task.cancelling()
661                ):
662                    raise
663        self._release_connection()
664
665    def _cleanup_writer(self) -> None:
666        if self.__writer is not None:
667            self.__writer.cancel()
668        self._session = None
669
670    def _notify_content(self) -> None:
671        content = self.content
672        if content and content.exception() is None:
673            set_exception(content, _CONNECTION_CLOSED_EXCEPTION)
674        self._released = True
675
676    async def wait_for_close(self) -> None:
677        if self.__writer is not None:
678            try:
679                await self.__writer
680            except asyncio.CancelledError:
681                if (
682                    sys.version_info >= (3, 11)
683                    and (task := asyncio.current_task())
684                    and task.cancelling()
685                ):
686                    raise
687        self.release()
688
689    async def read(self) -> bytes:
690        """Read response payload."""
691        if self._body is None:
692            try:
693                self._body = await self.content.read()
694                for trace in self._traces:
695                    await trace.send_response_chunk_received(
696                        self.method, self.url, self._body
697                    )
698            except BaseException:
699                self.close()
700                raise
701        elif self._released:  # Response explicitly released
702            raise ClientConnectionError("Connection closed")
703
704        protocol = self._connection and self._connection.protocol
705        if protocol is None or not protocol.upgraded:
706            await self._wait_released()  # Underlying connection released
707        return self._body
708
709    def get_encoding(self) -> str:
710        ctype = self.headers.get(hdrs.CONTENT_TYPE, "").lower()
711        mimetype = helpers.parse_mimetype(ctype)
712
713        encoding = mimetype.parameters.get("charset")
714        if encoding:
715            with contextlib.suppress(LookupError, ValueError):
716                return codecs.lookup(encoding).name
717
718        if mimetype.type == "application" and (
719            mimetype.subtype == "json" or mimetype.subtype == "rdap"
720        ):
721            # RFC 7159 states that the default encoding is UTF-8.
722            # RFC 7483 defines application/rdap+json
723            return "utf-8"
724
725        if self._body is None:
726            raise RuntimeError(
727                "Cannot compute fallback encoding of a not yet read body"
728            )
729
730        return self._resolve_charset(self, self._body)
731
732    async def text(self, encoding: Optional[str] = None, errors: str = "strict") -> str:
733        """Read response payload and decode."""
734        if self._body is None:
735            await self.read()
736
737        if encoding is None:
738            encoding = self.get_encoding()
739
740        return self._body.decode(encoding, errors=errors)  # type: ignore[union-attr]
741
742    async def json(
743        self,
744        *,
745        encoding: Optional[str] = None,
746        loads: JSONDecoder = DEFAULT_JSON_DECODER,
747        content_type: Optional[str] = "application/json",
748    ) -> Any:
749        """Read and decodes JSON response."""
750        if self._body is None:
751            await self.read()
752
753        if content_type:
754            ctype = self.headers.get(hdrs.CONTENT_TYPE, "").lower()
755            if not _is_expected_content_type(ctype, content_type):
756                raise ContentTypeError(
757                    self.request_info,
758                    self.history,
759                    status=self.status,
760                    message=(
761                        "Attempt to decode JSON with unexpected mimetype: %s" % ctype
762                    ),
763                    headers=self.headers,
764                )
765
766        stripped = self._body.strip()  # type: ignore[union-attr]
767        if not stripped:
768            return None
769
770        if encoding is None:
771            encoding = self.get_encoding()
772
773        return loads(stripped.decode(encoding))
774
775    async def __aenter__(self) -> "ClientResponse":
776        self._in_context = True
777        return self
778
779    async def __aexit__(
780        self,
781        exc_type: Optional[Type[BaseException]],
782        exc_val: Optional[BaseException],
783        exc_tb: Optional[TracebackType],
784    ) -> None:
785        self._in_context = False
786        # similar to _RequestContextManager, we do not need to check
787        # for exceptions, response object can close connection
788        # if state is broken
789        self.release()
790        await self.wait_for_close()
791
792
793class ClientRequest:
794    GET_METHODS = {
795        hdrs.METH_GET,
796        hdrs.METH_HEAD,
797        hdrs.METH_OPTIONS,
798        hdrs.METH_TRACE,
799    }
800    POST_METHODS = {hdrs.METH_PATCH, hdrs.METH_POST, hdrs.METH_PUT}
801    ALL_METHODS = GET_METHODS.union(POST_METHODS).union({hdrs.METH_DELETE})
802
803    DEFAULT_HEADERS = {
804        hdrs.ACCEPT: "*/*",
805        hdrs.ACCEPT_ENCODING: _gen_default_accept_encoding(),
806    }
807
808    # Type of body depends on PAYLOAD_REGISTRY, which is dynamic.
809    _body: Union[None, payload.Payload] = None
810    auth = None
811    response = None
812
813    __writer: Optional["asyncio.Task[None]"] = None  # async task for streaming data
814
815    # These class defaults help create_autospec() work correctly.
816    # If autospec is improved in future, maybe these can be removed.
817    url = URL()
818    method = "GET"
819
820    _continue = None  # waiter future for '100 Continue' response
821
822    _skip_auto_headers: Optional["CIMultiDict[None]"] = None
823
824    # N.B.
825    # Adding __del__ method with self._writer closing doesn't make sense
826    # because _writer is instance method, thus it keeps a reference to self.
827    # Until writer has finished finalizer will not be called.
828
829    def __init__(
830        self,
831        method: str,
832        url: URL,
833        *,
834        params: Query = None,
835        headers: Optional[LooseHeaders] = None,
836        skip_auto_headers: Optional[Iterable[str]] = None,
837        data: Any = None,
838        cookies: Optional[LooseCookies] = None,
839        auth: Optional[BasicAuth] = None,
840        version: http.HttpVersion = http.HttpVersion11,
841        compress: Union[str, bool, None] = None,
842        chunked: Optional[bool] = None,
843        expect100: bool = False,
844        loop: Optional[asyncio.AbstractEventLoop] = None,
845        response_class: Optional[Type["ClientResponse"]] = None,
846        proxy: Optional[URL] = None,
847        proxy_auth: Optional[BasicAuth] = None,
848        timer: Optional[BaseTimerContext] = None,
849        session: Optional["ClientSession"] = None,
850        ssl: Union[SSLContext, bool, Fingerprint] = True,
851        proxy_headers: Optional[LooseHeaders] = None,
852        traces: Optional[List["Trace"]] = None,
853        trust_env: bool = False,
854        server_hostname: Optional[str] = None,
855    ):
856        if loop is None:
857            loop = asyncio.get_event_loop()
858        if match := _CONTAINS_CONTROL_CHAR_RE.search(method):
859            raise ValueError(
860                f"Method cannot contain non-token characters {method!r} "
861                f"(found at least {match.group()!r})"
862            )
863        # URL forbids subclasses, so a simple type check is enough.
864        assert type(url) is URL, url
865        if proxy is not None:
866            assert type(proxy) is URL, proxy
867        # FIXME: session is None in tests only, need to fix tests
868        # assert session is not None
869        if TYPE_CHECKING:
870            assert session is not None
871        self._session = session
872        if params:
873            url = url.extend_query(params)
874        self.original_url = url
875        self.url = url.with_fragment(None) if url.raw_fragment else url
876        self.method = method.upper()
877        self.chunked = chunked
878        self.compress = compress
879        self.loop = loop
880        self.length = None
881        if response_class is None:
882            real_response_class = ClientResponse
883        else:
884            real_response_class = response_class
885        self.response_class: Type[ClientResponse] = real_response_class
886        self._timer = timer if timer is not None else TimerNoop()
887        self._ssl = ssl if ssl is not None else True
888        self.server_hostname = server_hostname
889
890        if loop.get_debug():
891            self._source_traceback = traceback.extract_stack(sys._getframe(1))
892
893        self.update_version(version)
894        self.update_host(url)
895        self.update_headers(headers)
896        self.update_auto_headers(skip_auto_headers)
897        self.update_cookies(cookies)
898        self.update_content_encoding(data)
899        self.update_auth(auth, trust_env)
900        self.update_proxy(proxy, proxy_auth, proxy_headers)
901
902        self.update_body_from_data(data)
903        if data is not None or self.method not in self.GET_METHODS:
904            self.update_transfer_encoding()
905        self.update_expect_continue(expect100)
906        self._traces = [] if traces is None else traces
907
908    def __reset_writer(self, _: object = None) -> None:
909        self.__writer = None
910
911    def _get_content_length(self) -> Optional[int]:
912        """Extract and validate Content-Length header value.
913
914        Returns parsed Content-Length value or None if not set.
915        Raises ValueError if header exists but cannot be parsed as an integer.
916        """
917        if hdrs.CONTENT_LENGTH not in self.headers:
918            return None
919
920        content_length_hdr = self.headers[hdrs.CONTENT_LENGTH]
921        try:
922            return int(content_length_hdr)
923        except ValueError:
924            raise ValueError(
925                f"Invalid Content-Length header: {content_length_hdr}"
926            ) from None
927
928    @property
929    def skip_auto_headers(self) -> CIMultiDict[None]:
930        return self._skip_auto_headers or CIMultiDict()
931
932    @property
933    def _writer(self) -> Optional["asyncio.Task[None]"]:
934        return self.__writer
935
936    @_writer.setter
937    def _writer(self, writer: "asyncio.Task[None]") -> None:
938        if self.__writer is not None:
939            self.__writer.remove_done_callback(self.__reset_writer)
940        self.__writer = writer
941        writer.add_done_callback(self.__reset_writer)
942
943    def is_ssl(self) -> bool:
944        return self.url.scheme in _SSL_SCHEMES
945
946    @property
947    def ssl(self) -> Union["SSLContext", bool, Fingerprint]:
948        return self._ssl
949
950    @property
951    def connection_key(self) -> ConnectionKey:
952        if proxy_headers := self.proxy_headers:
953            h: Optional[int] = hash(tuple(proxy_headers.items()))
954        else:
955            h = None
956        url = self.url
957        return tuple.__new__(
958            ConnectionKey,
959            (
960                url.raw_host or "",
961                url.port,
962                url.scheme in _SSL_SCHEMES,
963                self._ssl,
964                self.proxy,
965                self.proxy_auth,
966                h,
967            ),
968        )
969
970    @property
971    def host(self) -> str:
972        ret = self.url.raw_host
973        assert ret is not None
974        return ret
975
976    @property
977    def port(self) -> Optional[int]:
978        return self.url.port
979
980    @property
981    def body(self) -> Union[payload.Payload, Literal[b""]]:
982        """Request body."""
983        # empty body is represented as bytes for backwards compatibility
984        return self._body or b""
985
986    @body.setter
987    def body(self, value: Any) -> None:
988        """Set request body with warning for non-autoclose payloads.
989
990        WARNING: This setter must be called from within an event loop and is not
991        thread-safe. Setting body outside of an event loop may raise RuntimeError
992        when closing file-based payloads.
993
994        DEPRECATED: Direct assignment to body is deprecated and will be removed
995        in a future version. Use await update_body() instead for proper resource
996        management.
997        """
998        # Close existing payload if present
999        if self._body is not None:
1000            # Warn if the payload needs manual closing
1001            # stacklevel=3: user code -> body setter -> _warn_if_unclosed_payload
1002            _warn_if_unclosed_payload(self._body, stacklevel=3)
1003            # NOTE: In the future, when we remove sync close support,
1004            # this setter will need to be removed and only the async
1005            # update_body() method will be available. For now, we call
1006            # _close() for backwards compatibility.
1007            self._body._close()
1008        self._update_body(value)
1009
1010    @property
1011    def request_info(self) -> RequestInfo:
1012        headers: CIMultiDictProxy[str] = CIMultiDictProxy(self.headers)
1013        # These are created on every request, so we use a NamedTuple
1014        # for performance reasons. We don't use the RequestInfo.__new__
1015        # method because it has a different signature which is provided
1016        # for backwards compatibility only.
1017        return tuple.__new__(
1018            RequestInfo, (self.url, self.method, headers, self.original_url)
1019        )
1020
1021    @property
1022    def session(self) -> "ClientSession":
1023        """Return the ClientSession instance.
1024
1025        This property provides access to the ClientSession that initiated
1026        this request, allowing middleware to make additional requests
1027        using the same session.
1028        """
1029        return self._session
1030
1031    def update_host(self, url: URL) -> None:
1032        """Update destination host, port and connection type (ssl)."""
1033        # get host/port
1034        if not url.raw_host:
1035            raise InvalidURL(url)
1036
1037        # basic auth info
1038        if url.raw_user or url.raw_password:
1039            self.auth = helpers.BasicAuth(url.user or "", url.password or "")
1040
1041    def update_version(self, version: Union[http.HttpVersion, str]) -> None:
1042        """Convert request version to two elements tuple.
1043
1044        parser HTTP version '1.1' => (1, 1)
1045        """
1046        if isinstance(version, str):
1047            v = [part.strip() for part in version.split(".", 1)]
1048            try:
1049                version = http.HttpVersion(int(v[0]), int(v[1]))
1050            except ValueError:
1051                raise ValueError(
1052                    f"Can not parse http version number: {version}"
1053                ) from None
1054        self.version = version
1055
1056    def update_headers(self, headers: Optional[LooseHeaders]) -> None:
1057        """Update request headers."""
1058        self.headers: CIMultiDict[str] = CIMultiDict()
1059
1060        # Build the host header
1061        host = self.url.host_port_subcomponent
1062
1063        # host_port_subcomponent is None when the URL is a relative URL.
1064        # but we know we do not have a relative URL here.
1065        assert host is not None
1066        self.headers[hdrs.HOST] = host
1067
1068        if not headers:
1069            return
1070
1071        if isinstance(headers, (dict, MultiDictProxy, MultiDict)):
1072            headers = headers.items()
1073
1074        for key, value in headers:  # type: ignore[misc]
1075            # A special case for Host header
1076            if key in hdrs.HOST_ALL:
1077                self.headers[key] = value
1078            else:
1079                self.headers.add(key, value)
1080
1081    def update_auto_headers(self, skip_auto_headers: Optional[Iterable[str]]) -> None:
1082        if skip_auto_headers is not None:
1083            self._skip_auto_headers = CIMultiDict(
1084                (hdr, None) for hdr in sorted(skip_auto_headers)
1085            )
1086            used_headers = self.headers.copy()
1087            used_headers.extend(self._skip_auto_headers)  # type: ignore[arg-type]
1088        else:
1089            # Fast path when there are no headers to skip
1090            # which is the most common case.
1091            used_headers = self.headers
1092
1093        for hdr, val in self.DEFAULT_HEADERS.items():
1094            if hdr not in used_headers:
1095                self.headers[hdr] = val
1096
1097        if hdrs.USER_AGENT not in used_headers:
1098            self.headers[hdrs.USER_AGENT] = SERVER_SOFTWARE
1099
1100    def update_cookies(self, cookies: Optional[LooseCookies]) -> None:
1101        """Update request cookies header."""
1102        if not cookies:
1103            return
1104
1105        c = SimpleCookie()
1106        if hdrs.COOKIE in self.headers:
1107            # parse_cookie_header for RFC 6265 compliant Cookie header parsing
1108            c.update(parse_cookie_header(self.headers.get(hdrs.COOKIE, "")))
1109            del self.headers[hdrs.COOKIE]
1110
1111        if isinstance(cookies, Mapping):
1112            iter_cookies = cookies.items()
1113        else:
1114            iter_cookies = cookies  # type: ignore[assignment]
1115        for name, value in iter_cookies:
1116            if isinstance(value, Morsel):
1117                # Use helper to preserve coded_value exactly as sent by server
1118                c[name] = preserve_morsel_with_coded_value(value)
1119            else:
1120                c[name] = value  # type: ignore[assignment]
1121
1122        self.headers[hdrs.COOKIE] = c.output(header="", sep=";").strip()
1123
1124    def update_content_encoding(self, data: Any) -> None:
1125        """Set request content encoding."""
1126        if not data:
1127            # Don't compress an empty body.
1128            self.compress = None
1129            return
1130
1131        if self.headers.get(hdrs.CONTENT_ENCODING):
1132            if self.compress:
1133                raise ValueError(
1134                    "compress can not be set if Content-Encoding header is set"
1135                )
1136        elif self.compress:
1137            if not isinstance(self.compress, str):
1138                self.compress = "deflate"
1139            self.headers[hdrs.CONTENT_ENCODING] = self.compress
1140            self.chunked = True  # enable chunked, no need to deal with length
1141
1142    def update_transfer_encoding(self) -> None:
1143        """Analyze transfer-encoding header."""
1144        te = self.headers.get(hdrs.TRANSFER_ENCODING, "").lower()
1145
1146        if "chunked" in te:
1147            if self.chunked:
1148                raise ValueError(
1149                    "chunked can not be set "
1150                    'if "Transfer-Encoding: chunked" header is set'
1151                )
1152
1153        elif self.chunked:
1154            if hdrs.CONTENT_LENGTH in self.headers:
1155                raise ValueError(
1156                    "chunked can not be set if Content-Length header is set"
1157                )
1158
1159            self.headers[hdrs.TRANSFER_ENCODING] = "chunked"
1160
1161    def update_auth(self, auth: Optional[BasicAuth], trust_env: bool = False) -> None:
1162        """Set basic auth."""
1163        if auth is None:
1164            auth = self.auth
1165        if auth is None:
1166            return
1167
1168        if not isinstance(auth, helpers.BasicAuth):
1169            raise TypeError("BasicAuth() tuple is required instead")
1170
1171        self.headers[hdrs.AUTHORIZATION] = auth.encode()
1172
1173    def update_body_from_data(self, body: Any, _stacklevel: int = 3) -> None:
1174        """Update request body from data."""
1175        if self._body is not None:
1176            _warn_if_unclosed_payload(self._body, stacklevel=_stacklevel)
1177
1178        if body is None:
1179            self._body = None
1180            # Set Content-Length to 0 when body is None for methods that expect a body
1181            if (
1182                self.method not in self.GET_METHODS
1183                and not self.chunked
1184                and hdrs.CONTENT_LENGTH not in self.headers
1185            ):
1186                self.headers[hdrs.CONTENT_LENGTH] = "0"
1187            return
1188
1189        # FormData
1190        maybe_payload = body() if isinstance(body, FormData) else body
1191
1192        try:
1193            body_payload = payload.PAYLOAD_REGISTRY.get(maybe_payload, disposition=None)
1194        except payload.LookupError:
1195            body_payload = FormData(maybe_payload)()  # type: ignore[arg-type]
1196
1197        self._body = body_payload
1198        # enable chunked encoding if needed
1199        if not self.chunked and hdrs.CONTENT_LENGTH not in self.headers:
1200            if (size := body_payload.size) is not None:

Showing the first 1,200 of 1537 lines. Download the file for the rest.

codekingpro/portable-devtools · Team Ai