Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
response.py886 linesDownload Raw Back to urllib3
1from __future__ import absolute_import2 3import io4import logging5import sys6import warnings7import zlib8from contextlib import contextmanager9from socket import error as SocketError10from socket import timeout as SocketTimeout11 12try:13    try:14        import brotlicffi as brotli15    except ImportError:16        import brotli17except ImportError:18    brotli = None19 20from . import util21from ._collections import HTTPHeaderDict22from .connection import BaseSSLError, HTTPException23from .exceptions import (24    BodyNotHttplibCompatible,25    DecodeError,26    HTTPError,27    IncompleteRead,28    InvalidChunkLength,29    InvalidHeader,30    ProtocolError,31    ReadTimeoutError,32    ResponseNotChunked,33    SSLError,34)35from .packages import six36from .util.response import is_fp_closed, is_response_to_head37 38log = logging.getLogger(__name__)39 40 41class DeflateDecoder(object):42    def __init__(self):43        self._first_try = True44        self._data = b""45        self._obj = zlib.decompressobj()46 47    def __getattr__(self, name):48        return getattr(self._obj, name)49 50    def decompress(self, data):51        if not data:52            return data53 54        if not self._first_try:55            return self._obj.decompress(data)56 57        self._data += data58        try:59            decompressed = self._obj.decompress(data)60            if decompressed:61                self._first_try = False62                self._data = None63            return decompressed64        except zlib.error:65            self._first_try = False66            self._obj = zlib.decompressobj(-zlib.MAX_WBITS)67            try:68                return self.decompress(self._data)69            finally:70                self._data = None71 72 73class GzipDecoderState(object):74 75    FIRST_MEMBER = 076    OTHER_MEMBERS = 177    SWALLOW_DATA = 278 79 80class GzipDecoder(object):81    def __init__(self):82        self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS)83        self._state = GzipDecoderState.FIRST_MEMBER84 85    def __getattr__(self, name):86        return getattr(self._obj, name)87 88    def decompress(self, data):89        ret = bytearray()90        if self._state == GzipDecoderState.SWALLOW_DATA or not data:91            return bytes(ret)92        while True:93            try:94                ret += self._obj.decompress(data)95            except zlib.error:96                previous_state = self._state97                # Ignore data after the first error98                self._state = GzipDecoderState.SWALLOW_DATA99                if previous_state == GzipDecoderState.OTHER_MEMBERS:100                    # Allow trailing garbage acceptable in other gzip clients101                    return bytes(ret)102                raise103            data = self._obj.unused_data104            if not data:105                return bytes(ret)106            self._state = GzipDecoderState.OTHER_MEMBERS107            self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS)108 109 110if brotli is not None:111 112    class BrotliDecoder(object):113        # Supports both 'brotlipy' and 'Brotli' packages114        # since they share an import name. The top branches115        # are for 'brotlipy' and bottom branches for 'Brotli'116        def __init__(self):117            self._obj = brotli.Decompressor()118            if hasattr(self._obj, "decompress"):119                self.decompress = self._obj.decompress120            else:121                self.decompress = self._obj.process122 123        def flush(self):124            if hasattr(self._obj, "flush"):125                return self._obj.flush()126            return b""127 128 129class MultiDecoder(object):130    """131    From RFC7231:132        If one or more encodings have been applied to a representation, the133        sender that applied the encodings MUST generate a Content-Encoding134        header field that lists the content codings in the order in which135        they were applied.136    """137 138    def __init__(self, modes):139        self._decoders = [_get_decoder(m.strip()) for m in modes.split(",")]140 141    def flush(self):142        return self._decoders[0].flush()143 144    def decompress(self, data):145        for d in reversed(self._decoders):146            data = d.decompress(data)147        return data148 149 150def _get_decoder(mode):151    if "," in mode:152        return MultiDecoder(mode)153 154    if mode == "gzip":155        return GzipDecoder()156 157    if brotli is not None and mode == "br":158        return BrotliDecoder()159 160    return DeflateDecoder()161 162 163class HTTPResponse(io.IOBase):164    """165    HTTP Response container.166 167    Backwards-compatible with :class:`http.client.HTTPResponse` but the response ``body`` is168    loaded and decoded on-demand when the ``data`` property is accessed.  This169    class is also compatible with the Python standard library's :mod:`io`170    module, and can hence be treated as a readable object in the context of that171    framework.172 173    Extra parameters for behaviour not present in :class:`http.client.HTTPResponse`:174 175    :param preload_content:176        If True, the response's body will be preloaded during construction.177 178    :param decode_content:179        If True, will attempt to decode the body based on the180        'content-encoding' header.181 182    :param original_response:183        When this HTTPResponse wrapper is generated from an :class:`http.client.HTTPResponse`184        object, it's convenient to include the original for debug purposes. It's185        otherwise unused.186 187    :param retries:188        The retries contains the last :class:`~urllib3.util.retry.Retry` that189        was used during the request.190 191    :param enforce_content_length:192        Enforce content length checking. Body returned by server must match193        value of Content-Length header, if present. Otherwise, raise error.194    """195 196    CONTENT_DECODERS = ["gzip", "deflate"]197    if brotli is not None:198        CONTENT_DECODERS += ["br"]199    REDIRECT_STATUSES = [301, 302, 303, 307, 308]200 201    def __init__(202        self,203        body="",204        headers=None,205        status=0,206        version=0,207        reason=None,208        strict=0,209        preload_content=True,210        decode_content=True,211        original_response=None,212        pool=None,213        connection=None,214        msg=None,215        retries=None,216        enforce_content_length=False,217        request_method=None,218        request_url=None,219        auto_close=True,220    ):221 222        if isinstance(headers, HTTPHeaderDict):223            self.headers = headers224        else:225            self.headers = HTTPHeaderDict(headers)226        self.status = status227        self.version = version228        self.reason = reason229        self.strict = strict230        self.decode_content = decode_content231        self.retries = retries232        self.enforce_content_length = enforce_content_length233        self.auto_close = auto_close234 235        self._decoder = None236        self._body = None237        self._fp = None238        self._original_response = original_response239        self._fp_bytes_read = 0240        self.msg = msg241        self._request_url = request_url242 243        if body and isinstance(body, (six.string_types, bytes)):244            self._body = body245 246        self._pool = pool247        self._connection = connection248 249        if hasattr(body, "read"):250            self._fp = body251 252        # Are we using the chunked-style of transfer encoding?253        self.chunked = False254        self.chunk_left = None255        tr_enc = self.headers.get("transfer-encoding", "").lower()256        # Don't incur the penalty of creating a list and then discarding it257        encodings = (enc.strip() for enc in tr_enc.split(","))258        if "chunked" in encodings:259            self.chunked = True260 261        # Determine length of response262        self.length_remaining = self._init_length(request_method)263 264        # If requested, preload the body.265        if preload_content and not self._body:266            self._body = self.read(decode_content=decode_content)267 268    def get_redirect_location(self):269        """270        Should we redirect and where to?271 272        :returns: Truthy redirect location string if we got a redirect status273            code and valid location. ``None`` if redirect status and no274            location. ``False`` if not a redirect status code.275        """276        if self.status in self.REDIRECT_STATUSES:277            return self.headers.get("location")278 279        return False280 281    def release_conn(self):282        if not self._pool or not self._connection:283            return284 285        self._pool._put_conn(self._connection)286        self._connection = None287 288    def drain_conn(self):289        """290        Read and discard any remaining HTTP response data in the response connection.291 292        Unread data in the HTTPResponse connection blocks the connection from being released back to the pool.293        """294        try:295            self.read()296        except (HTTPError, SocketError, BaseSSLError, HTTPException):297            pass298 299    @property300    def data(self):301        # For backwards-compat with earlier urllib3 0.4 and earlier.302        if self._body:303            return self._body304 305        if self._fp:306            return self.read(cache_content=True)307 308    @property309    def connection(self):310        return self._connection311 312    def isclosed(self):313        return is_fp_closed(self._fp)314 315    def tell(self):316        """317        Obtain the number of bytes pulled over the wire so far. May differ from318        the amount of content returned by :meth:``urllib3.response.HTTPResponse.read``319        if bytes are encoded on the wire (e.g, compressed).320        """321        return self._fp_bytes_read322 323    def _init_length(self, request_method):324        """325        Set initial length value for Response content if available.326        """327        length = self.headers.get("content-length")328 329        if length is not None:330            if self.chunked:331                # This Response will fail with an IncompleteRead if it can't be332                # received as chunked. This method falls back to attempt reading333                # the response before raising an exception.334                log.warning(335                    "Received response with both Content-Length and "336                    "Transfer-Encoding set. This is expressly forbidden "337                    "by RFC 7230 sec 3.3.2. Ignoring Content-Length and "338                    "attempting to process response as Transfer-Encoding: "339                    "chunked."340                )341                return None342 343            try:344                # RFC 7230 section 3.3.2 specifies multiple content lengths can345                # be sent in a single Content-Length header346                # (e.g. Content-Length: 42, 42). This line ensures the values347                # are all valid ints and that as long as the `set` length is 1,348                # all values are the same. Otherwise, the header is invalid.349                lengths = set([int(val) for val in length.split(",")])350                if len(lengths) > 1:351                    raise InvalidHeader(352                        "Content-Length contained multiple "353                        "unmatching values (%s)" % length354                    )355                length = lengths.pop()356            except ValueError:357                length = None358            else:359                if length < 0:360                    length = None361 362        # Convert status to int for comparison363        # In some cases, httplib returns a status of "_UNKNOWN"364        try:365            status = int(self.status)366        except ValueError:367            status = 0368 369        # Check for responses that shouldn't include a body370        if status in (204, 304) or 100 <= status < 200 or request_method == "HEAD":371            length = 0372 373        return length374 375    def _init_decoder(self):376        """377        Set-up the _decoder attribute if necessary.378        """379        # Note: content-encoding value should be case-insensitive, per RFC 7230380        # Section 3.2381        content_encoding = self.headers.get("content-encoding", "").lower()382        if self._decoder is None:383            if content_encoding in self.CONTENT_DECODERS:384                self._decoder = _get_decoder(content_encoding)385            elif "," in content_encoding:386                encodings = [387                    e.strip()388                    for e in content_encoding.split(",")389                    if e.strip() in self.CONTENT_DECODERS390                ]391                if len(encodings):392                    self._decoder = _get_decoder(content_encoding)393 394    DECODER_ERROR_CLASSES = (IOError, zlib.error)395    if brotli is not None:396        DECODER_ERROR_CLASSES += (brotli.error,)397 398    def _decode(self, data, decode_content, flush_decoder):399        """400        Decode the data passed in and potentially flush the decoder.401        """402        if not decode_content:403            return data404 405        try:406            if self._decoder:407                data = self._decoder.decompress(data)408        except self.DECODER_ERROR_CLASSES as e:409            content_encoding = self.headers.get("content-encoding", "").lower()410            raise DecodeError(411                "Received response with content-encoding: %s, but "412                "failed to decode it." % content_encoding,413                e,414            )415        if flush_decoder:416            data += self._flush_decoder()417 418        return data419 420    def _flush_decoder(self):421        """422        Flushes the decoder. Should only be called if the decoder is actually423        being used.424        """425        if self._decoder:426            buf = self._decoder.decompress(b"")427            return buf + self._decoder.flush()428 429        return b""430 431    @contextmanager432    def _error_catcher(self):433        """434        Catch low-level python exceptions, instead re-raising urllib3435        variants, so that low-level exceptions are not leaked in the436        high-level api.437 438        On exit, release the connection back to the pool.439        """440        clean_exit = False441 442        try:443            try:444                yield445 446            except SocketTimeout:447                # FIXME: Ideally we'd like to include the url in the ReadTimeoutError but448                # there is yet no clean way to get at it from this context.449                raise ReadTimeoutError(self._pool, None, "Read timed out.")450 451            except BaseSSLError as e:452                # FIXME: Is there a better way to differentiate between SSLErrors?453                if "read operation timed out" not in str(e):454                    # SSL errors related to framing/MAC get wrapped and reraised here455                    raise SSLError(e)456 457                raise ReadTimeoutError(self._pool, None, "Read timed out.")458 459            except (HTTPException, SocketError) as e:460                # This includes IncompleteRead.461                raise ProtocolError("Connection broken: %r" % e, e)462 463            # If no exception is thrown, we should avoid cleaning up464            # unnecessarily.465            clean_exit = True466        finally:467            # If we didn't terminate cleanly, we need to throw away our468            # connection.469            if not clean_exit:470                # The response may not be closed but we're not going to use it471                # anymore so close it now to ensure that the connection is472                # released back to the pool.473                if self._original_response:474                    self._original_response.close()475 476                # Closing the response may not actually be sufficient to close477                # everything, so if we have a hold of the connection close that478                # too.479                if self._connection:480                    self._connection.close()481 482            # If we hold the original response but it's closed now, we should483            # return the connection back to the pool.484            if self._original_response and self._original_response.isclosed():485                self.release_conn()486 487    def _fp_read(self, amt):488        """489        Read a response with the thought that reading the number of bytes490        larger than can fit in a 32-bit int at a time via SSL in some491        known cases leads to an overflow error that has to be prevented492        if `amt` or `self.length_remaining` indicate that a problem may493        happen.494 495        The known cases:496          * 3.8 <= CPython < 3.9.7 because of a bug497            https://github.com/urllib3/urllib3/issues/2513#issuecomment-1152559900.498          * urllib3 injected with pyOpenSSL-backed SSL-support.499          * CPython < 3.10 only when `amt` does not fit 32-bit int.500        """501        assert self._fp502        c_int_max = 2 ** 31 - 1503        if (504            (505                (amt and amt > c_int_max)506                or (self.length_remaining and self.length_remaining > c_int_max)507            )508            and not util.IS_SECURETRANSPORT509            and (util.IS_PYOPENSSL or sys.version_info < (3, 10))510        ):511            buffer = io.BytesIO()512            # Besides `max_chunk_amt` being a maximum chunk size, it513            # affects memory overhead of reading a response by this514            # method in CPython.515            # `c_int_max` equal to 2 GiB - 1 byte is the actual maximum516            # chunk size that does not lead to an overflow error, but517            # 256 MiB is a compromise.518            max_chunk_amt = 2 ** 28519            while amt is None or amt != 0:520                if amt is not None:521                    chunk_amt = min(amt, max_chunk_amt)522                    amt -= chunk_amt523                else:524                    chunk_amt = max_chunk_amt525                data = self._fp.read(chunk_amt)526                if not data:527                    break528                buffer.write(data)529                del data  # to reduce peak memory usage by `max_chunk_amt`.530            return buffer.getvalue()531        else:532            # StringIO doesn't like amt=None533            return self._fp.read(amt) if amt is not None else self._fp.read()534 535    def read(self, amt=None, decode_content=None, cache_content=False):536        """537        Similar to :meth:`http.client.HTTPResponse.read`, but with two additional538        parameters: ``decode_content`` and ``cache_content``.539 540        :param amt:541            How much of the content to read. If specified, caching is skipped542            because it doesn't make sense to cache partial content as the full543            response.544 545        :param decode_content:546            If True, will attempt to decode the body based on the547            'content-encoding' header.548 549        :param cache_content:550            If True, will save the returned data such that the same result is551            returned despite of the state of the underlying file object. This552            is useful if you want the ``.data`` property to continue working553            after having ``.read()`` the file object. (Overridden if ``amt`` is554            set.)555        """556        self._init_decoder()557        if decode_content is None:558            decode_content = self.decode_content559 560        if self._fp is None:561            return562 563        flush_decoder = False564        fp_closed = getattr(self._fp, "closed", False)565 566        with self._error_catcher():567            data = self._fp_read(amt) if not fp_closed else b""568            if amt is None:569                flush_decoder = True570            else:571                cache_content = False572                if (573                    amt != 0 and not data574                ):  # Platform-specific: Buggy versions of Python.575                    # Close the connection when no data is returned576                    #577                    # This is redundant to what httplib/http.client _should_578                    # already do.  However, versions of python released before579                    # December 15, 2012 (http://bugs.python.org/issue16298) do580                    # not properly close the connection in all cases. There is581                    # no harm in redundantly calling close.582                    self._fp.close()583                    flush_decoder = True584                    if self.enforce_content_length and self.length_remaining not in (585                        0,586                        None,587                    ):588                        # This is an edge case that httplib failed to cover due589                        # to concerns of backward compatibility. We're590                        # addressing it here to make sure IncompleteRead is591                        # raised during streaming, so all calls with incorrect592                        # Content-Length are caught.593                        raise IncompleteRead(self._fp_bytes_read, self.length_remaining)594 595        if data:596            self._fp_bytes_read += len(data)597            if self.length_remaining is not None:598                self.length_remaining -= len(data)599 600            data = self._decode(data, decode_content, flush_decoder)601 602            if cache_content:603                self._body = data604 605        return data606 607    def stream(self, amt=2 ** 16, decode_content=None):608        """609        A generator wrapper for the read() method. A call will block until610        ``amt`` bytes have been read from the connection or until the611        connection is closed.612 613        :param amt:614            How much of the content to read. The generator will return up to615            much data per iteration, but may return less. This is particularly616            likely when using compressed data. However, the empty string will617            never be returned.618 619        :param decode_content:620            If True, will attempt to decode the body based on the621            'content-encoding' header.622        """623        if self.chunked and self.supports_chunked_reads():624            for line in self.read_chunked(amt, decode_content=decode_content):625                yield line626        else:627            while not is_fp_closed(self._fp):628                data = self.read(amt=amt, decode_content=decode_content)629 630                if data:631                    yield data632 633    @classmethod634    def from_httplib(ResponseCls, r, **response_kw):635        """636        Given an :class:`http.client.HTTPResponse` instance ``r``, return a637        corresponding :class:`urllib3.response.HTTPResponse` object.638 639        Remaining parameters are passed to the HTTPResponse constructor, along640        with ``original_response=r``.641        """642        headers = r.msg643 644        if not isinstance(headers, HTTPHeaderDict):645            if six.PY2:646                # Python 2.7647                headers = HTTPHeaderDict.from_httplib(headers)648            else:649                headers = HTTPHeaderDict(headers.items())650 651        # HTTPResponse objects in Python 3 don't have a .strict attribute652        strict = getattr(r, "strict", 0)653        resp = ResponseCls(654            body=r,655            headers=headers,656            status=r.status,657            version=r.version,658            reason=r.reason,659            strict=strict,660            original_response=r,661            **response_kw662        )663        return resp664 665    # Backwards-compatibility methods for http.client.HTTPResponse666    def getheaders(self):667        warnings.warn(668            "HTTPResponse.getheaders() is deprecated and will be removed "669            "in urllib3 v2.1.0. Instead access HTTPResponse.headers directly.",670            category=DeprecationWarning,671            stacklevel=2,672        )673        return self.headers674 675    def getheader(self, name, default=None):676        warnings.warn(677            "HTTPResponse.getheader() is deprecated and will be removed "678            "in urllib3 v2.1.0. Instead use HTTPResponse.headers.get(name, default).",679            category=DeprecationWarning,680            stacklevel=2,681        )682        return self.headers.get(name, default)683 684    # Backwards compatibility for http.cookiejar685    def info(self):686        return self.headers687 688    # Overrides from io.IOBase689    def close(self):690        if not self.closed:691            self._fp.close()692 693        if self._connection:694            self._connection.close()695 696        if not self.auto_close:697            io.IOBase.close(self)698 699    @property700    def closed(self):701        if not self.auto_close:702            return io.IOBase.closed.__get__(self)703        elif self._fp is None:704            return True705        elif hasattr(self._fp, "isclosed"):706            return self._fp.isclosed()707        elif hasattr(self._fp, "closed"):708            return self._fp.closed709        else:710            return True711 712    def fileno(self):713        if self._fp is None:714            raise IOError("HTTPResponse has no file to get a fileno from")715        elif hasattr(self._fp, "fileno"):716            return self._fp.fileno()717        else:718            raise IOError(719                "The file-like object this HTTPResponse is wrapped "720                "around has no file descriptor"721            )722 723    def flush(self):724        if (725            self._fp is not None726            and hasattr(self._fp, "flush")727            and not getattr(self._fp, "closed", False)728        ):729            return self._fp.flush()730 731    def readable(self):732        # This method is required for `io` module compatibility.733        return True734 735    def readinto(self, b):736        # This method is required for `io` module compatibility.737        temp = self.read(len(b))738        if len(temp) == 0:739            return 0740        else:741            b[: len(temp)] = temp742            return len(temp)743 744    def supports_chunked_reads(self):745        """746        Checks if the underlying file-like object looks like a747        :class:`http.client.HTTPResponse` object. We do this by testing for748        the fp attribute. If it is present we assume it returns raw chunks as749        processed by read_chunked().750        """751        return hasattr(self._fp, "fp")752 753    def _update_chunk_length(self):754        # First, we'll figure out length of a chunk and then755        # we'll try to read it from socket.756        if self.chunk_left is not None:757            return758        line = self._fp.fp.readline()759        line = line.split(b";", 1)[0]760        try:761            self.chunk_left = int(line, 16)762        except ValueError:763            # Invalid chunked protocol response, abort.764            self.close()765            raise InvalidChunkLength(self, line)766 767    def _handle_chunk(self, amt):768        returned_chunk = None769        if amt is None:770            chunk = self._fp._safe_read(self.chunk_left)771            returned_chunk = chunk772            self._fp._safe_read(2)  # Toss the CRLF at the end of the chunk.773            self.chunk_left = None774        elif amt < self.chunk_left:775            value = self._fp._safe_read(amt)776            self.chunk_left = self.chunk_left - amt777            returned_chunk = value778        elif amt == self.chunk_left:779            value = self._fp._safe_read(amt)780            self._fp._safe_read(2)  # Toss the CRLF at the end of the chunk.781            self.chunk_left = None782            returned_chunk = value783        else:  # amt > self.chunk_left784            returned_chunk = self._fp._safe_read(self.chunk_left)785            self._fp._safe_read(2)  # Toss the CRLF at the end of the chunk.786            self.chunk_left = None787        return returned_chunk788 789    def read_chunked(self, amt=None, decode_content=None):790        """791        Similar to :meth:`HTTPResponse.read`, but with an additional792        parameter: ``decode_content``.793 794        :param amt:795            How much of the content to read. If specified, caching is skipped796            because it doesn't make sense to cache partial content as the full797            response.798 799        :param decode_content:800            If True, will attempt to decode the body based on the801            'content-encoding' header.802        """803        self._init_decoder()804        # FIXME: Rewrite this method and make it a class with a better structured logic.805        if not self.chunked:806            raise ResponseNotChunked(807                "Response is not chunked. "808                "Header 'transfer-encoding: chunked' is missing."809            )810        if not self.supports_chunked_reads():811            raise BodyNotHttplibCompatible(812                "Body should be http.client.HTTPResponse like. "813                "It should have have an fp attribute which returns raw chunks."814            )815 816        with self._error_catcher():817            # Don't bother reading the body of a HEAD request.818            if self._original_response and is_response_to_head(self._original_response):819                self._original_response.close()820                return821 822            # If a response is already read and closed823            # then return immediately.824            if self._fp.fp is None:825                return826 827            while True:828                self._update_chunk_length()829                if self.chunk_left == 0:830                    break831                chunk = self._handle_chunk(amt)832                decoded = self._decode(833                    chunk, decode_content=decode_content, flush_decoder=False834                )835                if decoded:836                    yield decoded837 838            if decode_content:839                # On CPython and PyPy, we should never need to flush the840                # decoder. However, on Jython we *might* need to, so841                # lets defensively do it anyway.842                decoded = self._flush_decoder()843                if decoded:  # Platform-specific: Jython.844                    yield decoded845 846            # Chunk content ends with \r\n: discard it.847            while True:848                line = self._fp.fp.readline()849                if not line:850                    # Some sites may not end with '\r\n'.851                    break852                if line == b"\r\n":853                    break854 855            # We read everything; close the "file".856            if self._original_response:857                self._original_response.close()858 859    def geturl(self):860        """861        Returns the URL that was the source of this response.862        If the request that generated this response redirected, this method863        will return the final redirect location.864        """865        if self.retries is not None and len(self.retries.history):866            return self.retries.history[-1].redirect_location867        else:868            return self._request_url869 870    def __iter__(self):871        buffer = []872        for chunk in self.stream(decode_content=True):873            if b"\n" in chunk:874                chunk = chunk.split(b"\n")875                yield b"".join(buffer) + chunk[0] + b"\n"876                for x in chunk[1:-1]:877                    yield x + b"\n"878                if chunk[-1]:879                    buffer = [chunk[-1]]880                else:881                    buffer = []882            else:883                buffer.append(chunk)884        if buffer:885            yield b"".join(buffer)886 
codekingpro/portable-devtools · Team Ai