VYPR
High severityNVD Advisory· Published Dec 5, 2025· Updated Dec 5, 2025

urllib3 Streaming API improperly handles highly compressed data

CVE-2025-66471

Description

urllib3 is a user-friendly HTTP client library for Python. Starting in version 1.0 and prior to 2.6.0, the Streaming API improperly handles highly compressed data. urllib3's streaming API is designed for the efficient handling of large HTTP responses by reading the content in chunks, rather than loading the entire response body into memory at once. When streaming a compressed response, urllib3 can perform decoding or decompression based on the HTTP Content-Encoding header (e.g., gzip, deflate, br, or zstd). The library must read compressed data from the network and decompress it until the requested chunk size is met. Any resulting decompressed data that exceeds the requested amount is held in an internal buffer for the next read operation. The decompression logic could cause urllib3 to fully decode a small amount of highly compressed data in a single operation. This can result in excessive resource consumption (high CPU usage and massive memory allocation for the decompressed data.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
urllib3PyPI
>= 1.0, < 2.6.02.6.0

Affected products

1

Patches

1
c19571de34c4

Merge commit from fork

https://github.com/urllib3/urllib3Illia VolochiiDec 5, 2025via ghsa
8 files changed · +621 154
  • CHANGES.rst+22 0 modified
    @@ -1,3 +1,25 @@
    +2.6.0 (TBD)
    +==================
    +
    +Bugfixes
    +--------
    +
    +- Fixed a security issue where streaming API could improperly handle highly
    +  compressed HTTP content ("decompression bombs") leading to excessive resource
    +  consumption even when a small amount of data was requested. Reading small
    +  chunks of compressed data is safer and much more efficient now.
    +
    +.. caution::
    +  - If urllib3 is not installed with the optional `urllib3[brotli]` extra, but
    +    your environment contains a Brotli/brotlicffi/brotlipy package anyway, make
    +    sure to upgrade it to at least Brotli 1.2.0 or brotlicffi 1.2.0.0 to
    +    benefit from the security fixes and avoid warnings. Prefer using
    +    `urllib3[brotli]` to install a compatible Brotli package automatically.
    +
    +  - If you use custom decompressors, please make sure to update them to
    +    respect the changed API of ``urllib3.response.ContentDecoder``.
    +
    +
     2.5.0 (2025-06-18)
     ==================
     
    
  • docs/advanced-usage.rst+2 1 modified
    @@ -66,7 +66,8 @@ When using ``preload_content=True`` (the default setting) the
     response body will be read immediately into memory and the HTTP connection
     will be released back into the pool without manual intervention.
     
    -However, when dealing with large responses it's often better to stream the response
    +However, when dealing with responses of large or unknown length,
    +it's often better to stream the response
     content using ``preload_content=False``. Setting ``preload_content`` to ``False`` means
     that urllib3 will only read from the socket when data is requested.
     
    
  • docs/user-guide.rst+2 2 modified
    @@ -145,8 +145,8 @@ to a byte string representing the response content:
         print(resp.data)
         # b"\xaa\xa5H?\x95\xe9\x9b\x11"
     
    -.. note:: For larger responses, it's sometimes better to :ref:`stream <stream>`
    -    the response.
    +.. note:: For responses of large or unknown length, it's sometimes better to
    +    :ref:`stream <stream>` the response.
     
     Using io Wrappers with Response Content
     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    
  • noxfile.py+12 4 modified
    @@ -15,6 +15,7 @@
     def tests_impl(
         session: nox.Session,
         extras: str | None = None,
    +    extra_dependencies: list[str] | None = None,
         # hypercorn dependency h2 compares bytes and strings
         # https://github.com/python-hyper/h2/issues/1236
         byte_string_comparisons: bool = False,
    @@ -33,8 +34,9 @@ def tests_impl(
         implementation_name, release_level, _is_gil_enabled = session_python_info.split(" ")
         free_threading = _is_gil_enabled == "False"
     
    -    # brotlicffi does not support free-threading
    -    extras = "socks,zstd,h2" if free_threading else "socks,brotli,zstd,h2"
    +    if extras is None:
    +        # brotlicffi does not support free-threading
    +        extras = "socks,zstd,h2" if free_threading else "socks,brotli,zstd,h2"
     
         # Install deps and the package itself.
         session.run_install(
    @@ -45,6 +47,8 @@ def tests_impl(
             dependency_group,
             *(f"--extra={extra}" for extra in (extras.split(",") if extras else ())),
         )
    +    if extra_dependencies:
    +        session.install(*extra_dependencies)
         # Show the uv version.
         session.run("uv", "--version")
         # Print the Python version, bytesize and free-threading status.
    @@ -130,8 +134,12 @@ def test_brotlipy(session: nox.Session) -> None:
         'brotlicffi' that we still don't blow up.
         """
         session.env["UV_PROJECT_ENVIRONMENT"] = session.virtualenv.location
    -    session.install("brotlipy")
    -    tests_impl(session, extras="socks", byte_string_comparisons=False)
    +    tests_impl(
    +        session,
    +        extras="socks",
    +        extra_dependencies=["brotlipy"],
    +        byte_string_comparisons=False,
    +    )
     
     
     def git_clone(session: nox.Session, git_url: str) -> None:
    
  • pyproject.toml+3 2 modified
    @@ -43,8 +43,8 @@ dynamic = ["version"]
     
     [project.optional-dependencies]
     brotli = [
    -  "brotli>=1.0.9; platform_python_implementation == 'CPython'",
    -  "brotlicffi>=0.8.0; platform_python_implementation != 'CPython'"
    +  "brotli>=1.2.0; platform_python_implementation == 'CPython'",
    +  "brotlicffi>=1.2.0.0; platform_python_implementation != 'CPython'"
     ]
     zstd = [
       "backports.zstd>=1.0.0; python_version<'3.14'",
    @@ -158,6 +158,7 @@ filterwarnings = [
         '''default:ssl\.PROTOCOL_TLSv1_1 is deprecated:DeprecationWarning''',
         '''default:ssl\.PROTOCOL_TLSv1_2 is deprecated:DeprecationWarning''',
         '''default:ssl NPN is deprecated, use ALPN instead:DeprecationWarning''',
    +    '''default:Brotli >= 1.2.0 is required to prevent decompression bombs\.:urllib3.exceptions.DependencyWarning''',
         # https://github.com/SeleniumHQ/selenium/issues/13328
         '''default:unclosed file <_io\.BufferedWriter name='/dev/null'>:ResourceWarning''',
         # https://github.com/SeleniumHQ/selenium/issues/14686
    
  • src/urllib3/response.py+240 39 modified
    @@ -7,6 +7,7 @@
     import socket
     import sys
     import typing
    +import warnings
     import zlib
     from contextlib import contextmanager
     from http.client import HTTPMessage as _HttplibHTTPMessage
    @@ -31,6 +32,7 @@
     from .exceptions import (
         BodyNotHttplibCompatible,
         DecodeError,
    +    DependencyWarning,
         HTTPError,
         IncompleteRead,
         InvalidChunkLength,
    @@ -50,7 +52,11 @@
     
     
     class ContentDecoder:
    -    def decompress(self, data: bytes) -> bytes:
    +    def decompress(self, data: bytes, max_length: int = -1) -> bytes:
    +        raise NotImplementedError()
    +
    +    @property
    +    def has_unconsumed_tail(self) -> bool:
             raise NotImplementedError()
     
         def flush(self) -> bytes:
    @@ -60,30 +66,57 @@ def flush(self) -> bytes:
     class DeflateDecoder(ContentDecoder):
         def __init__(self) -> None:
             self._first_try = True
    -        self._data = b""
    +        self._first_try_data = b""
    +        self._unfed_data = b""
             self._obj = zlib.decompressobj()
     
    -    def decompress(self, data: bytes) -> bytes:
    -        if not data:
    +    def decompress(self, data: bytes, max_length: int = -1) -> bytes:
    +        data = self._unfed_data + data
    +        self._unfed_data = b""
    +        if not data and not self._obj.unconsumed_tail:
                 return data
    +        original_max_length = max_length
    +        if original_max_length < 0:
    +            max_length = 0
    +        elif original_max_length == 0:
    +            # We should not pass 0 to the zlib decompressor because 0 is
    +            # the default value that will make zlib decompress without a
    +            # length limit.
    +            # Data should be stored for subsequent calls.
    +            self._unfed_data = data
    +            return b""
     
    +        # Subsequent calls always reuse `self._obj`. zlib requires
    +        # passing the unconsumed tail if decompression is to continue.
             if not self._first_try:
    -            return self._obj.decompress(data)
    +            return self._obj.decompress(
    +                self._obj.unconsumed_tail + data, max_length=max_length
    +            )
     
    -        self._data += data
    +        # First call tries with RFC 1950 ZLIB format.
    +        self._first_try_data += data
             try:
    -            decompressed = self._obj.decompress(data)
    +            decompressed = self._obj.decompress(data, max_length=max_length)
                 if decompressed:
                     self._first_try = False
    -                self._data = None  # type: ignore[assignment]
    +                self._first_try_data = b""
                 return decompressed
    +        # On failure, it falls back to RFC 1951 DEFLATE format.
             except zlib.error:
                 self._first_try = False
                 self._obj = zlib.decompressobj(-zlib.MAX_WBITS)
                 try:
    -                return self.decompress(self._data)
    +                return self.decompress(
    +                    self._first_try_data, max_length=original_max_length
    +                )
                 finally:
    -                self._data = None  # type: ignore[assignment]
    +                self._first_try_data = b""
    +
    +    @property
    +    def has_unconsumed_tail(self) -> bool:
    +        return bool(self._unfed_data) or (
    +            bool(self._obj.unconsumed_tail) and not self._first_try
    +        )
     
         def flush(self) -> bytes:
             return self._obj.flush()
    @@ -99,27 +132,61 @@ class GzipDecoder(ContentDecoder):
         def __init__(self) -> None:
             self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS)
             self._state = GzipDecoderState.FIRST_MEMBER
    +        self._unconsumed_tail = b""
     
    -    def decompress(self, data: bytes) -> bytes:
    +    def decompress(self, data: bytes, max_length: int = -1) -> bytes:
             ret = bytearray()
    -        if self._state == GzipDecoderState.SWALLOW_DATA or not data:
    +        if self._state == GzipDecoderState.SWALLOW_DATA:
    +            return bytes(ret)
    +
    +        if max_length == 0:
    +            # We should not pass 0 to the zlib decompressor because 0 is
    +            # the default value that will make zlib decompress without a
    +            # length limit.
    +            # Data should be stored for subsequent calls.
    +            self._unconsumed_tail += data
    +            return b""
    +
    +        # zlib requires passing the unconsumed tail to the subsequent
    +        # call if decompression is to continue.
    +        data = self._unconsumed_tail + data
    +        if not data and self._obj.eof:
                 return bytes(ret)
    +
             while True:
                 try:
    -                ret += self._obj.decompress(data)
    +                ret += self._obj.decompress(
    +                    data, max_length=max(max_length - len(ret), 0)
    +                )
                 except zlib.error:
                     previous_state = self._state
                     # Ignore data after the first error
                     self._state = GzipDecoderState.SWALLOW_DATA
    +                self._unconsumed_tail = b""
                     if previous_state == GzipDecoderState.OTHER_MEMBERS:
                         # Allow trailing garbage acceptable in other gzip clients
                         return bytes(ret)
                     raise
    -            data = self._obj.unused_data
    +
    +            self._unconsumed_tail = data = (
    +                self._obj.unconsumed_tail or self._obj.unused_data
    +            )
    +            if max_length > 0 and len(ret) >= max_length:
    +                break
    +
                 if not data:
                     return bytes(ret)
    -            self._state = GzipDecoderState.OTHER_MEMBERS
    -            self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS)
    +            # When the end of a gzip member is reached, a new decompressor
    +            # must be created for unused (possibly future) data.
    +            if self._obj.eof:
    +                self._state = GzipDecoderState.OTHER_MEMBERS
    +                self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS)
    +
    +        return bytes(ret)
    +
    +    @property
    +    def has_unconsumed_tail(self) -> bool:
    +        return bool(self._unconsumed_tail)
     
         def flush(self) -> bytes:
             return self._obj.flush()
    @@ -134,9 +201,35 @@ class BrotliDecoder(ContentDecoder):
             def __init__(self) -> None:
                 self._obj = brotli.Decompressor()
                 if hasattr(self._obj, "decompress"):
    -                setattr(self, "decompress", self._obj.decompress)
    +                setattr(self, "_decompress", self._obj.decompress)
                 else:
    -                setattr(self, "decompress", self._obj.process)
    +                setattr(self, "_decompress", self._obj.process)
    +
    +        # Requires Brotli >= 1.2.0 for `output_buffer_limit`.
    +        def _decompress(self, data: bytes, output_buffer_limit: int = -1) -> bytes:
    +            raise NotImplementedError()
    +
    +        def decompress(self, data: bytes, max_length: int = -1) -> bytes:
    +            try:
    +                if max_length > 0:
    +                    return self._decompress(data, output_buffer_limit=max_length)
    +                else:
    +                    return self._decompress(data)
    +            except TypeError:
    +                # Fallback for Brotli/brotlicffi/brotlipy versions without
    +                # the `output_buffer_limit` parameter.
    +                warnings.warn(
    +                    "Brotli >= 1.2.0 is required to prevent decompression bombs.",
    +                    DependencyWarning,
    +                )
    +                return self._decompress(data)
    +
    +        @property
    +        def has_unconsumed_tail(self) -> bool:
    +            try:
    +                return not self._obj.can_accept_more_data()
    +            except AttributeError:
    +                return False
     
             def flush(self) -> bytes:
                 if hasattr(self._obj, "flush"):
    @@ -158,16 +251,46 @@ class ZstdDecoder(ContentDecoder):
             def __init__(self) -> None:
                 self._obj = zstd.ZstdDecompressor()
     
    -        def decompress(self, data: bytes) -> bytes:
    -            if not data:
    +        def decompress(self, data: bytes, max_length: int = -1) -> bytes:
    +            if not data and not self.has_unconsumed_tail:
                     return b""
    -            data_parts = [self._obj.decompress(data)]
    -            while self._obj.eof and self._obj.unused_data:
    -                unused_data = self._obj.unused_data
    +            if self._obj.eof:
    +                data = self._obj.unused_data + data
                     self._obj = zstd.ZstdDecompressor()
    -                data_parts.append(self._obj.decompress(unused_data))
    +            part = self._obj.decompress(data, max_length=max_length)
    +            length = len(part)
    +            data_parts = [part]
    +            # Every loop iteration is supposed to read data from a separate frame.
    +            # The loop breaks when:
    +            #   - enough data is read;
    +            #   - no more unused data is available;
    +            #   - end of the last read frame has not been reached (i.e.,
    +            #     more data has to be fed).
    +            while (
    +                self._obj.eof
    +                and self._obj.unused_data
    +                and (max_length < 0 or length < max_length)
    +            ):
    +                unused_data = self._obj.unused_data
    +                if not self._obj.needs_input:
    +                    self._obj = zstd.ZstdDecompressor()
    +                part = self._obj.decompress(
    +                    unused_data,
    +                    max_length=(max_length - length) if max_length > 0 else -1,
    +                )
    +                if part_length := len(part):
    +                    data_parts.append(part)
    +                    length += part_length
    +                elif self._obj.needs_input:
    +                    break
                 return b"".join(data_parts)
     
    +        @property
    +        def has_unconsumed_tail(self) -> bool:
    +            return not (self._obj.needs_input or self._obj.eof) or bool(
    +                self._obj.unused_data
    +            )
    +
             def flush(self) -> bytes:
                 if not self._obj.eof:
                     raise DecodeError("Zstandard data is incomplete")
    @@ -189,10 +312,35 @@ def __init__(self, modes: str) -> None:
         def flush(self) -> bytes:
             return self._decoders[0].flush()
     
    -    def decompress(self, data: bytes) -> bytes:
    -        for d in reversed(self._decoders):
    -            data = d.decompress(data)
    -        return data
    +    def decompress(self, data: bytes, max_length: int = -1) -> bytes:
    +        if max_length <= 0:
    +            for d in reversed(self._decoders):
    +                data = d.decompress(data)
    +            return data
    +
    +        ret = bytearray()
    +        # Every while loop iteration goes through all decoders once.
    +        # It exits when enough data is read or no more data can be read.
    +        # It is possible that the while loop iteration does not produce
    +        # any data because we retrieve up to `max_length` from every
    +        # decoder, and the amount of bytes may be insufficient for the
    +        # next decoder to produce enough/any output.
    +        while True:
    +            any_data = False
    +            for d in reversed(self._decoders):
    +                data = d.decompress(data, max_length=max_length - len(ret))
    +                if data:
    +                    any_data = True
    +                # We should not break when no data is returned because
    +                # next decoders may produce data even with empty input.
    +            ret += data
    +            if not any_data or len(ret) >= max_length:
    +                return bytes(ret)
    +            data = b""
    +
    +    @property
    +    def has_unconsumed_tail(self) -> bool:
    +        return any(d.has_unconsumed_tail for d in self._decoders)
     
     
     def _get_decoder(mode: str) -> ContentDecoder:
    @@ -225,9 +373,6 @@ class BytesQueueBuffer:
     
          * self.buffer, which contains the full data
          * the largest chunk that we will copy in get()
    -
    -    The worst case scenario is a single chunk, in which case we'll make a full copy of
    -    the data inside get().
         """
     
         def __init__(self) -> None:
    @@ -249,6 +394,10 @@ def get(self, n: int) -> bytes:
             elif n < 0:
                 raise ValueError("n should be > 0")
     
    +        if len(self.buffer[0]) == n and isinstance(self.buffer[0], bytes):
    +            self._size -= n
    +            return self.buffer.popleft()
    +
             fetched = 0
             ret = io.BytesIO()
             while fetched < n:
    @@ -458,7 +607,11 @@ def _init_decoder(self) -> None:
                         self._decoder = _get_decoder(content_encoding)
     
         def _decode(
    -        self, data: bytes, decode_content: bool | None, flush_decoder: bool
    +        self,
    +        data: bytes,
    +        decode_content: bool | None,
    +        flush_decoder: bool,
    +        max_length: int | None = None,
         ) -> bytes:
             """
             Decode the data passed in and potentially flush the decoder.
    @@ -471,9 +624,12 @@ def _decode(
                     )
                 return data
     
    +        if max_length is None or flush_decoder:
    +            max_length = -1
    +
             try:
                 if self._decoder:
    -                data = self._decoder.decompress(data)
    +                data = self._decoder.decompress(data, max_length=max_length)
                     self._has_decoded_content = True
             except self.DECODER_ERROR_CLASSES as e:
                 content_encoding = self.headers.get("content-encoding", "").lower()
    @@ -921,14 +1077,26 @@ def read(
             elif amt is not None:
                 cache_content = False
     
    +            if self._decoder and self._decoder.has_unconsumed_tail:
    +                decoded_data = self._decode(
    +                    b"",
    +                    decode_content,
    +                    flush_decoder=False,
    +                    max_length=amt - len(self._decoded_buffer),
    +                )
    +                self._decoded_buffer.put(decoded_data)
                 if len(self._decoded_buffer) >= amt:
                     return self._decoded_buffer.get(amt)
     
             data = self._raw_read(amt)
     
             flush_decoder = amt is None or (amt != 0 and not data)
     
    -        if not data and len(self._decoded_buffer) == 0:
    +        if (
    +            not data
    +            and len(self._decoded_buffer) == 0
    +            and not (self._decoder and self._decoder.has_unconsumed_tail)
    +        ):
                 return data
     
             if amt is None:
    @@ -945,15 +1113,25 @@ def read(
                         )
                     return data
     
    -            decoded_data = self._decode(data, decode_content, flush_decoder)
    +            decoded_data = self._decode(
    +                data,
    +                decode_content,
    +                flush_decoder,
    +                max_length=amt - len(self._decoded_buffer),
    +            )
                 self._decoded_buffer.put(decoded_data)
     
                 while len(self._decoded_buffer) < amt and data:
                     # TODO make sure to initially read enough data to get past the headers
                     # For example, the GZ file header takes 10 bytes, we don't want to read
                     # it one byte at a time
                     data = self._raw_read(amt)
    -                decoded_data = self._decode(data, decode_content, flush_decoder)
    +                decoded_data = self._decode(
    +                    data,
    +                    decode_content,
    +                    flush_decoder,
    +                    max_length=amt - len(self._decoded_buffer),
    +                )
                     self._decoded_buffer.put(decoded_data)
                 data = self._decoded_buffer.get(amt)
     
    @@ -988,6 +1166,20 @@ def read1(
                         "Calling read1(decode_content=False) is not supported after "
                         "read1(decode_content=True) was called."
                     )
    +            if (
    +                self._decoder
    +                and self._decoder.has_unconsumed_tail
    +                and (amt is None or len(self._decoded_buffer) < amt)
    +            ):
    +                decoded_data = self._decode(
    +                    b"",
    +                    decode_content,
    +                    flush_decoder=False,
    +                    max_length=(
    +                        amt - len(self._decoded_buffer) if amt is not None else None
    +                    ),
    +                )
    +                self._decoded_buffer.put(decoded_data)
                 if len(self._decoded_buffer) > 0:
                     if amt is None:
                         return self._decoded_buffer.get_all()
    @@ -1003,7 +1195,9 @@ def read1(
             self._init_decoder()
             while True:
                 flush_decoder = not data
    -            decoded_data = self._decode(data, decode_content, flush_decoder)
    +            decoded_data = self._decode(
    +                data, decode_content, flush_decoder, max_length=amt
    +            )
                 self._decoded_buffer.put(decoded_data)
                 if decoded_data or flush_decoder:
                     break
    @@ -1034,7 +1228,11 @@ def stream(
             if self.chunked and self.supports_chunked_reads():
                 yield from self.read_chunked(amt, decode_content=decode_content)
             else:
    -            while not is_fp_closed(self._fp) or len(self._decoded_buffer) > 0:
    +            while (
    +                not is_fp_closed(self._fp)
    +                or len(self._decoded_buffer) > 0
    +                or (self._decoder and self._decoder.has_unconsumed_tail)
    +            ):
                     data = self.read(amt=amt, decode_content=decode_content)
     
                     if data:
    @@ -1197,7 +1395,10 @@ def read_chunked(
                         break
                     chunk = self._handle_chunk(amt)
                     decoded = self._decode(
    -                    chunk, decode_content=decode_content, flush_decoder=False
    +                    chunk,
    +                    decode_content=decode_content,
    +                    flush_decoder=False,
    +                    max_length=amt,
                     )
                     if decoded:
                         yield decoded
    
  • test/test_response.py+264 5 modified
    @@ -1,6 +1,7 @@
     from __future__ import annotations
     
     import contextlib
    +import gzip
     import http.client as httplib
     import socket
     import ssl
    @@ -43,6 +44,26 @@ def zstd_compress(data: bytes) -> bytes:
         return zstd.compress(data)
     
     
    +def deflate2_compress(data: bytes) -> bytes:
    +    compressor = zlib.compressobj(6, zlib.DEFLATED, -zlib.MAX_WBITS)
    +    return compressor.compress(data) + compressor.flush()
    +
    +
    +if brotli:
    +    try:
    +        brotli.Decompressor().process(b"", output_buffer_limit=1024)
    +        _brotli_gte_1_2_0_available = True
    +    except (AttributeError, TypeError):
    +        _brotli_gte_1_2_0_available = False
    +else:
    +    _brotli_gte_1_2_0_available = False
    +try:
    +    zstd_compress(b"")
    +    _zstd_available = True
    +except ModuleNotFoundError:
    +    _zstd_available = False
    +
    +
     class TestBytesQueueBuffer:
         def test_single_chunk(self) -> None:
             buffer = BytesQueueBuffer()
    @@ -120,12 +141,19 @@ def test_memory_usage(
             assert type(result) is bytes
             assert len(result) == 10 * 2**20
     
    +    @pytest.mark.parametrize(
    +        "get_func",
    +        (lambda b: b.get(len(b)), lambda b: b.get_all()),
    +        ids=("get", "get_all"),
    +    )
         @pytest.mark.limit_memory("10.01 MB", current_thread_only=True)
    -    def test_get_all_memory_usage_single_chunk(self) -> None:
    +    def test_memory_usage_single_chunk(
    +        self, get_func: typing.Callable[[BytesQueueBuffer], bytes]
    +    ) -> None:
             buffer = BytesQueueBuffer()
             chunk = bytes(10 * 2**20)  # 10 MiB
             buffer.put(chunk)
    -        assert buffer.get_all() is chunk
    +        assert get_func(buffer) is chunk
     
         @pytest.mark.parametrize(
             "finish_with_get_all",
    @@ -430,7 +458,26 @@ def test_decode_zstd(self) -> None:
             assert r.data == b"foo"
     
         @onlyZstd()
    -    def test_decode_multiframe_zstd(self) -> None:
    +    @pytest.mark.parametrize(
    +        "read_amt",
    +        (
    +            # Read all data at once.
    +            None,
    +            # Read one byte at a time, data of frames will be returned
    +            # separately.
    +            1,
    +            # Read two bytes at a time, the second read should return
    +            # data from both frames.
    +            2,
    +            # Read three bytes at a time, the whole frames will be
    +            # returned separately in two calls.
    +            3,
    +            # Read four bytes at a time, the first read should return
    +            # data from the first frame and a part of the second frame.
    +            4,
    +        ),
    +    )
    +    def test_decode_multiframe_zstd(self, read_amt: int | None) -> None:
             data = (
                 # Zstandard frame
                 zstd_compress(b"foo")
    @@ -445,8 +492,57 @@ def test_decode_multiframe_zstd(self) -> None:
             )
     
             fp = BytesIO(data)
    -        r = HTTPResponse(fp, headers={"content-encoding": "zstd"})
    -        assert r.data == b"foobar"
    +        result = bytearray()
    +        r = HTTPResponse(
    +            fp, headers={"content-encoding": "zstd"}, preload_content=False
    +        )
    +        total_length = 6
    +        while len(result) < total_length:
    +            chunk = r.read(read_amt, decode_content=True)
    +            if read_amt is None:
    +                assert len(chunk) == total_length
    +            else:
    +                assert len(chunk) == min(read_amt, total_length - len(result))
    +            result += chunk
    +        assert bytes(result) == b"foobar"
    +
    +    @onlyZstd()
    +    def test_decode_multiframe_zstd_with_max_length_close_to_compressed_data_size(
    +        self,
    +    ) -> None:
    +        """
    +        Test decoding when the first read from the socket returns all
    +        the compressed frames, but then it has to be decompressed in a
    +        couple of read calls.
    +        """
    +        data = (
    +            # Zstandard frame
    +            zstd_compress(b"x" * 1024)
    +            # skippable frame (must be ignored)
    +            + bytes.fromhex(
    +                "50 2A 4D 18"  # Magic_Number (little-endian)
    +                "07 00 00 00"  # Frame_Size (little-endian)
    +                "00 00 00 00 00 00 00"  # User_Data
    +            )
    +            # Zstandard frame
    +            + zstd_compress(b"y" * 1024)
    +        )
    +
    +        fp = BytesIO(data)
    +        r = HTTPResponse(
    +            fp, headers={"content-encoding": "zstd"}, preload_content=False
    +        )
    +        # Read the whole first frame.
    +        assert r.read(1024) == b"x" * 1024
    +        assert len(r._decoded_buffer) == 0
    +        # Read the whole second frame in two reads.
    +        assert r.read(512) == b"y" * 512
    +        assert len(r._decoded_buffer) == 0
    +        assert r.read(512) == b"y" * 512
    +        assert len(r._decoded_buffer) == 0
    +        # Ensure no more data is left.
    +        assert r.read() == b""
    +        assert len(r._decoded_buffer) == 0
     
         @onlyZstd()
         def test_chunked_decoding_zstd(self) -> None:
    @@ -539,6 +635,169 @@ def test_decode_zstd_read1(self, data: bytes) -> None:
                 decoded_data += part
             assert decoded_data == data
     
    +    _test_compressor_params: list[
    +        tuple[str, tuple[str, typing.Callable[[bytes], bytes]] | None]
    +    ] = [
    +        ("deflate1", ("deflate", zlib.compress)),
    +        ("deflate2", ("deflate", deflate2_compress)),
    +        ("gzip", ("gzip", gzip.compress)),
    +    ]
    +    if _brotli_gte_1_2_0_available:
    +        _test_compressor_params.append(("brotli", ("br", brotli.compress)))
    +    else:
    +        _test_compressor_params.append(("brotli", None))
    +    if _zstd_available:
    +        _test_compressor_params.append(("zstd", ("zstd", zstd_compress)))
    +    else:
    +        _test_compressor_params.append(("zstd", None))
    +
    +    @pytest.mark.parametrize("read_method", ("read", "read1"))
    +    @pytest.mark.parametrize(
    +        "data",
    +        [d[1] for d in _test_compressor_params],
    +        ids=[d[0] for d in _test_compressor_params],
    +    )
    +    def test_read_with_all_data_already_in_decompressor(
    +        self,
    +        request: pytest.FixtureRequest,
    +        read_method: str,
    +        data: tuple[str, typing.Callable[[bytes], bytes]] | None,
    +    ) -> None:
    +        if data is None:
    +            pytest.skip(f"Proper {request.node.callspec.id} decoder is not available")
    +        original_data = b"bar" * 1000
    +        name, compress_func = data
    +        compressed_data = compress_func(original_data)
    +        fp = mock.Mock(read=mock.Mock(return_value=b""))
    +        r = HTTPResponse(fp, headers={"content-encoding": name}, preload_content=False)
    +        # Put all data in the decompressor's buffer.
    +        r._init_decoder()
    +        assert r._decoder is not None  # for mypy
    +        decoded = r._decoder.decompress(compressed_data, max_length=0)
    +        if name == "br":
    +            # It's known that some Brotli libraries do not respect
    +            # `max_length`.
    +            r._decoded_buffer.put(decoded)
    +        else:
    +            assert decoded == b""
    +        # Read the data via `HTTPResponse`.
    +        read = getattr(r, read_method)
    +        assert read(0) == b""
    +        assert read(2500) == original_data[:2500]
    +        assert read(500) == original_data[2500:]
    +        assert read(0) == b""
    +        assert read() == b""
    +
    +    @pytest.mark.parametrize(
    +        "delta",
    +        (
    +            0,  # First read from socket returns all compressed data.
    +            -1,  # First read from socket returns all but one byte of compressed data.
    +        ),
    +    )
    +    @pytest.mark.parametrize("read_method", ("read", "read1"))
    +    @pytest.mark.parametrize(
    +        "data",
    +        [d[1] for d in _test_compressor_params],
    +        ids=[d[0] for d in _test_compressor_params],
    +    )
    +    def test_decode_with_max_length_close_to_compressed_data_size(
    +        self,
    +        request: pytest.FixtureRequest,
    +        delta: int,
    +        read_method: str,
    +        data: tuple[str, typing.Callable[[bytes], bytes]] | None,
    +    ) -> None:
    +        """
    +        Test decoding when the first read from the socket returns all or
    +        almost all the compressed data, but then it has to be
    +        decompressed in a couple of read calls.
    +        """
    +        if data is None:
    +            pytest.skip(f"Proper {request.node.callspec.id} decoder is not available")
    +
    +        original_data = b"foo" * 1000
    +        name, compress_func = data
    +        compressed_data = compress_func(original_data)
    +        fp = BytesIO(compressed_data)
    +        r = HTTPResponse(fp, headers={"content-encoding": name}, preload_content=False)
    +        initial_limit = len(compressed_data) + delta
    +        read = getattr(r, read_method)
    +        initial_chunk = read(amt=initial_limit, decode_content=True)
    +        assert len(initial_chunk) == initial_limit
    +        assert (
    +            len(read(amt=len(original_data), decode_content=True))
    +            == len(original_data) - initial_limit
    +        )
    +
    +    # Prepare 50 MB of compressed data outside of the test measuring
    +    # memory usage.
    +    _test_memory_usage_decode_with_max_length_params: list[
    +        tuple[str, tuple[str, bytes] | None]
    +    ] = [
    +        (
    +            params[0],
    +            (params[1][0], params[1][1](b"A" * (50 * 2**20))) if params[1] else None,
    +        )
    +        for params in _test_compressor_params
    +    ]
    +
    +    @pytest.mark.parametrize(
    +        "data",
    +        [d[1] for d in _test_memory_usage_decode_with_max_length_params],
    +        ids=[d[0] for d in _test_memory_usage_decode_with_max_length_params],
    +    )
    +    @pytest.mark.parametrize("read_method", ("read", "read1", "read_chunked", "stream"))
    +    # Decoders consume different amounts of memory during decompression.
    +    # We set the 10 MB limit to ensure that the whole decompressed data
    +    # is not stored unnecessarily.
    +    #
    +    # FYI, the following consumption was observed for the test with
    +    # `read` on CPython 3.14.0:
    +    #   - deflate: 2.3 MiB
    +    #   - deflate2: 2.1 MiB
    +    #   - gzip: 2.1 MiB
    +    #   - brotli:
    +    #     - brotli v1.2.0: 9 MiB
    +    #     - brotlicffi v1.2.0.0: 6 MiB
    +    #     - brotlipy v0.7.0: 105.8 MiB
    +    #   - zstd: 4.5 MiB
    +    @pytest.mark.limit_memory("10 MB", current_thread_only=True)
    +    def test_memory_usage_decode_with_max_length(
    +        self,
    +        request: pytest.FixtureRequest,
    +        read_method: str,
    +        data: tuple[str, bytes] | None,
    +    ) -> None:
    +        if data is None:
    +            pytest.skip(f"Proper {request.node.callspec.id} decoder is not available")
    +
    +        name, compressed_data = data
    +        limit = 1024 * 1024  # 1 MiB
    +        if read_method in ("read_chunked", "stream"):
    +            httplib_r = httplib.HTTPResponse(MockSock)  # type: ignore[arg-type]
    +            httplib_r.fp = MockChunkedEncodingResponse([compressed_data])  # type: ignore[assignment]
    +            r = HTTPResponse(
    +                httplib_r,
    +                preload_content=False,
    +                headers={"transfer-encoding": "chunked", "content-encoding": name},
    +            )
    +            next(getattr(r, read_method)(amt=limit, decode_content=True))
    +        else:
    +            fp = BytesIO(compressed_data)
    +            r = HTTPResponse(
    +                fp, headers={"content-encoding": name}, preload_content=False
    +            )
    +            getattr(r, read_method)(amt=limit, decode_content=True)
    +
    +        # Check that the internal decoded buffer is empty unless brotli
    +        # is used.
    +        # Google's brotli library does not fully respect the output
    +        # buffer limit: https://github.com/google/brotli/issues/1396
    +        # And unmaintained brotlipy cannot limit the output buffer size.
    +        if name != "br" or brotli.__name__ == "brotlicffi":
    +            assert len(r._decoded_buffer) == 0
    +
         def test_multi_decoding_deflate_deflate(self) -> None:
             data = zlib.compress(zlib.compress(b"foo"))
     
    
  • uv.lock+76 101 modified
    @@ -243,115 +243,90 @@ wheels = [
     
     [[package]]
     name = "brotli"
    -version = "1.1.0"
    +version = "1.2.0"
     source = { registry = "https://pypi.org/simple" }
    -sdist = { url = "https://files.pythonhosted.org/packages/2f/c2/f9e977608bdf958650638c3f1e28f85a1b075f075ebbe77db8555463787b/Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", size = 7372270, upload-time = "2023-09-07T14:05:41.643Z" }
    -wheels = [
    -    { url = "https://files.pythonhosted.org/packages/6d/3a/dbf4fb970c1019a57b5e492e1e0eae745d32e59ba4d6161ab5422b08eefe/Brotli-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e1140c64812cb9b06c922e77f1c26a75ec5e3f0fb2bf92cc8c58720dec276752", size = 873045, upload-time = "2023-09-07T14:03:16.894Z" },
    -    { url = "https://files.pythonhosted.org/packages/dd/11/afc14026ea7f44bd6eb9316d800d439d092c8d508752055ce8d03086079a/Brotli-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8fd5270e906eef71d4a8d19b7c6a43760c6abcfcc10c9101d14eb2357418de9", size = 446218, upload-time = "2023-09-07T14:03:18.917Z" },
    -    { url = "https://files.pythonhosted.org/packages/36/83/7545a6e7729db43cb36c4287ae388d6885c85a86dd251768a47015dfde32/Brotli-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ae56aca0402a0f9a3431cddda62ad71666ca9d4dc3a10a142b9dce2e3c0cda3", size = 2903872, upload-time = "2023-09-07T14:03:20.398Z" },
    -    { url = "https://files.pythonhosted.org/packages/32/23/35331c4d9391fcc0f29fd9bec2c76e4b4eeab769afbc4b11dd2e1098fb13/Brotli-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ce1b9935bfa1ede40028054d7f48b5469cd02733a365eec8a329ffd342915d", size = 2941254, upload-time = "2023-09-07T14:03:21.914Z" },
    -    { url = "https://files.pythonhosted.org/packages/3b/24/1671acb450c902edb64bd765d73603797c6c7280a9ada85a195f6b78c6e5/Brotli-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c4855522edb2e6ae7fdb58e07c3ba9111e7621a8956f481c68d5d979c93032e", size = 2857293, upload-time = "2023-09-07T14:03:24Z" },
    -    { url = "https://files.pythonhosted.org/packages/d5/00/40f760cc27007912b327fe15bf6bfd8eaecbe451687f72a8abc587d503b3/Brotli-1.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:38025d9f30cf4634f8309c6874ef871b841eb3c347e90b0851f63d1ded5212da", size = 3002385, upload-time = "2023-09-07T14:03:26.248Z" },
    -    { url = "https://files.pythonhosted.org/packages/b8/cb/8aaa83f7a4caa131757668c0fb0c4b6384b09ffa77f2fba9570d87ab587d/Brotli-1.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6a904cb26bfefc2f0a6f240bdf5233be78cd2488900a2f846f3c3ac8489ab80", size = 2911104, upload-time = "2023-09-07T14:03:27.849Z" },
    -    { url = "https://files.pythonhosted.org/packages/bc/c4/65456561d89d3c49f46b7fbeb8fe6e449f13bdc8ea7791832c5d476b2faf/Brotli-1.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a37b8f0391212d29b3a91a799c8e4a2855e0576911cdfb2515487e30e322253d", size = 2809981, upload-time = "2023-09-07T14:03:29.92Z" },
    -    { url = "https://files.pythonhosted.org/packages/05/1b/cf49528437bae28abce5f6e059f0d0be6fecdcc1d3e33e7c54b3ca498425/Brotli-1.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e84799f09591700a4154154cab9787452925578841a94321d5ee8fb9a9a328f0", size = 2935297, upload-time = "2023-09-07T14:03:32.035Z" },
    -    { url = "https://files.pythonhosted.org/packages/81/ff/190d4af610680bf0c5a09eb5d1eac6e99c7c8e216440f9c7cfd42b7adab5/Brotli-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f66b5337fa213f1da0d9000bc8dc0cb5b896b726eefd9c6046f699b169c41b9e", size = 2930735, upload-time = "2023-09-07T14:03:33.801Z" },
    -    { url = "https://files.pythonhosted.org/packages/80/7d/f1abbc0c98f6e09abd3cad63ec34af17abc4c44f308a7a539010f79aae7a/Brotli-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5dab0844f2cf82be357a0eb11a9087f70c5430b2c241493fc122bb6f2bb0917c", size = 2933107, upload-time = "2024-10-18T12:32:09.016Z" },
    -    { url = "https://files.pythonhosted.org/packages/34/ce/5a5020ba48f2b5a4ad1c0522d095ad5847a0be508e7d7569c8630ce25062/Brotli-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4fe605b917c70283db7dfe5ada75e04561479075761a0b3866c081d035b01c1", size = 2845400, upload-time = "2024-10-18T12:32:11.134Z" },
    -    { url = "https://files.pythonhosted.org/packages/44/89/fa2c4355ab1eecf3994e5a0a7f5492c6ff81dfcb5f9ba7859bd534bb5c1a/Brotli-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1e9a65b5736232e7a7f91ff3d02277f11d339bf34099a56cdab6a8b3410a02b2", size = 3031985, upload-time = "2024-10-18T12:32:12.813Z" },
    -    { url = "https://files.pythonhosted.org/packages/af/a4/79196b4a1674143d19dca400866b1a4d1a089040df7b93b88ebae81f3447/Brotli-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:58d4b711689366d4a03ac7957ab8c28890415e267f9b6589969e74b6e42225ec", size = 2927099, upload-time = "2024-10-18T12:32:14.733Z" },
    -    { url = "https://files.pythonhosted.org/packages/e9/54/1c0278556a097f9651e657b873ab08f01b9a9ae4cac128ceb66427d7cd20/Brotli-1.1.0-cp310-cp310-win32.whl", hash = "sha256:be36e3d172dc816333f33520154d708a2657ea63762ec16b62ece02ab5e4daf2", size = 333172, upload-time = "2023-09-07T14:03:35.212Z" },
    -    { url = "https://files.pythonhosted.org/packages/f7/65/b785722e941193fd8b571afd9edbec2a9b838ddec4375d8af33a50b8dab9/Brotli-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:0c6244521dda65ea562d5a69b9a26120769b7a9fb3db2fe9545935ed6735b128", size = 357255, upload-time = "2023-09-07T14:03:36.447Z" },
    -    { url = "https://files.pythonhosted.org/packages/96/12/ad41e7fadd5db55459c4c401842b47f7fee51068f86dd2894dd0dcfc2d2a/Brotli-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3daabb76a78f829cafc365531c972016e4aa8d5b4bf60660ad8ecee19df7ccc", size = 873068, upload-time = "2023-09-07T14:03:37.779Z" },
    -    { url = "https://files.pythonhosted.org/packages/95/4e/5afab7b2b4b61a84e9c75b17814198ce515343a44e2ed4488fac314cd0a9/Brotli-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c8146669223164fc87a7e3de9f81e9423c67a79d6b3447994dfb9c95da16e2d6", size = 446244, upload-time = "2023-09-07T14:03:39.223Z" },
    -    { url = "https://files.pythonhosted.org/packages/9d/e6/f305eb61fb9a8580c525478a4a34c5ae1a9bcb12c3aee619114940bc513d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30924eb4c57903d5a7526b08ef4a584acc22ab1ffa085faceb521521d2de32dd", size = 2906500, upload-time = "2023-09-07T14:03:40.858Z" },
    -    { url = "https://files.pythonhosted.org/packages/3e/4f/af6846cfbc1550a3024e5d3775ede1e00474c40882c7bf5b37a43ca35e91/Brotli-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceb64bbc6eac5a140ca649003756940f8d6a7c444a68af170b3187623b43bebf", size = 2943950, upload-time = "2023-09-07T14:03:42.896Z" },
    -    { url = "https://files.pythonhosted.org/packages/b3/e7/ca2993c7682d8629b62630ebf0d1f3bb3d579e667ce8e7ca03a0a0576a2d/Brotli-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a469274ad18dc0e4d316eefa616d1d0c2ff9da369af19fa6f3daa4f09671fd61", size = 2918527, upload-time = "2023-09-07T14:03:44.552Z" },
    -    { url = "https://files.pythonhosted.org/packages/b3/96/da98e7bedc4c51104d29cc61e5f449a502dd3dbc211944546a4cc65500d3/Brotli-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524f35912131cc2cabb00edfd8d573b07f2d9f21fa824bd3fb19725a9cf06327", size = 2845489, upload-time = "2023-09-07T14:03:46.594Z" },
    -    { url = "https://files.pythonhosted.org/packages/e8/ef/ccbc16947d6ce943a7f57e1a40596c75859eeb6d279c6994eddd69615265/Brotli-1.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5b3cc074004d968722f51e550b41a27be656ec48f8afaeeb45ebf65b561481dd", size = 2914080, upload-time = "2023-09-07T14:03:48.204Z" },
    -    { url = "https://files.pythonhosted.org/packages/80/d6/0bd38d758d1afa62a5524172f0b18626bb2392d717ff94806f741fcd5ee9/Brotli-1.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:19c116e796420b0cee3da1ccec3b764ed2952ccfcc298b55a10e5610ad7885f9", size = 2813051, upload-time = "2023-09-07T14:03:50.348Z" },
    -    { url = "https://files.pythonhosted.org/packages/14/56/48859dd5d129d7519e001f06dcfbb6e2cf6db92b2702c0c2ce7d97e086c1/Brotli-1.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:510b5b1bfbe20e1a7b3baf5fed9e9451873559a976c1a78eebaa3b86c57b4265", size = 2938172, upload-time = "2023-09-07T14:03:52.395Z" },
    -    { url = "https://files.pythonhosted.org/packages/3d/77/a236d5f8cd9e9f4348da5acc75ab032ab1ab2c03cc8f430d24eea2672888/Brotli-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a1fd8a29719ccce974d523580987b7f8229aeace506952fa9ce1d53a033873c8", size = 2933023, upload-time = "2023-09-07T14:03:53.96Z" },
    -    { url = "https://files.pythonhosted.org/packages/f1/87/3b283efc0f5cb35f7f84c0c240b1e1a1003a5e47141a4881bf87c86d0ce2/Brotli-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c247dd99d39e0338a604f8c2b3bc7061d5c2e9e2ac7ba9cc1be5a69cb6cd832f", size = 2935871, upload-time = "2024-10-18T12:32:16.688Z" },
    -    { url = "https://files.pythonhosted.org/packages/f3/eb/2be4cc3e2141dc1a43ad4ca1875a72088229de38c68e842746b342667b2a/Brotli-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1b2c248cd517c222d89e74669a4adfa5577e06ab68771a529060cf5a156e9757", size = 2847784, upload-time = "2024-10-18T12:32:18.459Z" },
    -    { url = "https://files.pythonhosted.org/packages/66/13/b58ddebfd35edde572ccefe6890cf7c493f0c319aad2a5badee134b4d8ec/Brotli-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2a24c50840d89ded6c9a8fdc7b6ed3692ed4e86f1c4a4a938e1e92def92933e0", size = 3034905, upload-time = "2024-10-18T12:32:20.192Z" },
    -    { url = "https://files.pythonhosted.org/packages/84/9c/bc96b6c7db824998a49ed3b38e441a2cae9234da6fa11f6ed17e8cf4f147/Brotli-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f31859074d57b4639318523d6ffdca586ace54271a73ad23ad021acd807eb14b", size = 2929467, upload-time = "2024-10-18T12:32:21.774Z" },
    -    { url = "https://files.pythonhosted.org/packages/e7/71/8f161dee223c7ff7fea9d44893fba953ce97cf2c3c33f78ba260a91bcff5/Brotli-1.1.0-cp311-cp311-win32.whl", hash = "sha256:39da8adedf6942d76dc3e46653e52df937a3c4d6d18fdc94a7c29d263b1f5b50", size = 333169, upload-time = "2023-09-07T14:03:55.404Z" },
    -    { url = "https://files.pythonhosted.org/packages/02/8a/fece0ee1057643cb2a5bbf59682de13f1725f8482b2c057d4e799d7ade75/Brotli-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:aac0411d20e345dc0920bdec5548e438e999ff68d77564d5e9463a7ca9d3e7b1", size = 357253, upload-time = "2023-09-07T14:03:56.643Z" },
    -    { url = "https://files.pythonhosted.org/packages/5c/d0/5373ae13b93fe00095a58efcbce837fd470ca39f703a235d2a999baadfbc/Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28", size = 815693, upload-time = "2024-10-18T12:32:23.824Z" },
    -    { url = "https://files.pythonhosted.org/packages/8e/48/f6e1cdf86751300c288c1459724bfa6917a80e30dbfc326f92cea5d3683a/Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f", size = 422489, upload-time = "2024-10-18T12:32:25.641Z" },
    -    { url = "https://files.pythonhosted.org/packages/06/88/564958cedce636d0f1bed313381dfc4b4e3d3f6015a63dae6146e1b8c65c/Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409", size = 873081, upload-time = "2023-09-07T14:03:57.967Z" },
    -    { url = "https://files.pythonhosted.org/packages/58/79/b7026a8bb65da9a6bb7d14329fd2bd48d2b7f86d7329d5cc8ddc6a90526f/Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2", size = 446244, upload-time = "2023-09-07T14:03:59.319Z" },
    -    { url = "https://files.pythonhosted.org/packages/e5/18/c18c32ecea41b6c0004e15606e274006366fe19436b6adccc1ae7b2e50c2/Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451", size = 2906505, upload-time = "2023-09-07T14:04:01.327Z" },
    -    { url = "https://files.pythonhosted.org/packages/08/c8/69ec0496b1ada7569b62d85893d928e865df29b90736558d6c98c2031208/Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91", size = 2944152, upload-time = "2023-09-07T14:04:03.033Z" },
    -    { url = "https://files.pythonhosted.org/packages/ab/fb/0517cea182219d6768113a38167ef6d4eb157a033178cc938033a552ed6d/Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408", size = 2919252, upload-time = "2023-09-07T14:04:04.675Z" },
    -    { url = "https://files.pythonhosted.org/packages/c7/53/73a3431662e33ae61a5c80b1b9d2d18f58dfa910ae8dd696e57d39f1a2f5/Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0", size = 2845955, upload-time = "2023-09-07T14:04:06.585Z" },
    -    { url = "https://files.pythonhosted.org/packages/55/ac/bd280708d9c5ebdbf9de01459e625a3e3803cce0784f47d633562cf40e83/Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc", size = 2914304, upload-time = "2023-09-07T14:04:08.668Z" },
    -    { url = "https://files.pythonhosted.org/packages/76/58/5c391b41ecfc4527d2cc3350719b02e87cb424ef8ba2023fb662f9bf743c/Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180", size = 2814452, upload-time = "2023-09-07T14:04:10.736Z" },
    -    { url = "https://files.pythonhosted.org/packages/c7/4e/91b8256dfe99c407f174924b65a01f5305e303f486cc7a2e8a5d43c8bec3/Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248", size = 2938751, upload-time = "2023-09-07T14:04:12.875Z" },
    -    { url = "https://files.pythonhosted.org/packages/5a/a6/e2a39a5d3b412938362bbbeba5af904092bf3f95b867b4a3eb856104074e/Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966", size = 2933757, upload-time = "2023-09-07T14:04:14.551Z" },
    -    { url = "https://files.pythonhosted.org/packages/13/f0/358354786280a509482e0e77c1a5459e439766597d280f28cb097642fc26/Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9", size = 2936146, upload-time = "2024-10-18T12:32:27.257Z" },
    -    { url = "https://files.pythonhosted.org/packages/80/f7/daf538c1060d3a88266b80ecc1d1c98b79553b3f117a485653f17070ea2a/Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb", size = 2848055, upload-time = "2024-10-18T12:32:29.376Z" },
    -    { url = "https://files.pythonhosted.org/packages/ad/cf/0eaa0585c4077d3c2d1edf322d8e97aabf317941d3a72d7b3ad8bce004b0/Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111", size = 3035102, upload-time = "2024-10-18T12:32:31.371Z" },
    -    { url = "https://files.pythonhosted.org/packages/d8/63/1c1585b2aa554fe6dbce30f0c18bdbc877fa9a1bf5ff17677d9cca0ac122/Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839", size = 2930029, upload-time = "2024-10-18T12:32:33.293Z" },
    -    { url = "https://files.pythonhosted.org/packages/5f/3b/4e3fd1893eb3bbfef8e5a80d4508bec17a57bb92d586c85c12d28666bb13/Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0", size = 333276, upload-time = "2023-09-07T14:04:16.49Z" },
    -    { url = "https://files.pythonhosted.org/packages/3d/d5/942051b45a9e883b5b6e98c041698b1eb2012d25e5948c58d6bf85b1bb43/Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951", size = 357255, upload-time = "2023-09-07T14:04:17.83Z" },
    -    { url = "https://files.pythonhosted.org/packages/0a/9f/fb37bb8ffc52a8da37b1c03c459a8cd55df7a57bdccd8831d500e994a0ca/Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5", size = 815681, upload-time = "2024-10-18T12:32:34.942Z" },
    -    { url = "https://files.pythonhosted.org/packages/06/b3/dbd332a988586fefb0aa49c779f59f47cae76855c2d00f450364bb574cac/Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8", size = 422475, upload-time = "2024-10-18T12:32:36.485Z" },
    -    { url = "https://files.pythonhosted.org/packages/bb/80/6aaddc2f63dbcf2d93c2d204e49c11a9ec93a8c7c63261e2b4bd35198283/Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f", size = 2906173, upload-time = "2024-10-18T12:32:37.978Z" },
    -    { url = "https://files.pythonhosted.org/packages/ea/1d/e6ca79c96ff5b641df6097d299347507d39a9604bde8915e76bf026d6c77/Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648", size = 2943803, upload-time = "2024-10-18T12:32:39.606Z" },
    -    { url = "https://files.pythonhosted.org/packages/ac/a3/d98d2472e0130b7dd3acdbb7f390d478123dbf62b7d32bda5c830a96116d/Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0", size = 2918946, upload-time = "2024-10-18T12:32:41.679Z" },
    -    { url = "https://files.pythonhosted.org/packages/c4/a5/c69e6d272aee3e1423ed005d8915a7eaa0384c7de503da987f2d224d0721/Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089", size = 2845707, upload-time = "2024-10-18T12:32:43.478Z" },
    -    { url = "https://files.pythonhosted.org/packages/58/9f/4149d38b52725afa39067350696c09526de0125ebfbaab5acc5af28b42ea/Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368", size = 2936231, upload-time = "2024-10-18T12:32:45.224Z" },
    -    { url = "https://files.pythonhosted.org/packages/5a/5a/145de884285611838a16bebfdb060c231c52b8f84dfbe52b852a15780386/Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c", size = 2848157, upload-time = "2024-10-18T12:32:46.894Z" },
    -    { url = "https://files.pythonhosted.org/packages/50/ae/408b6bfb8525dadebd3b3dd5b19d631da4f7d46420321db44cd99dcf2f2c/Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284", size = 3035122, upload-time = "2024-10-18T12:32:48.844Z" },
    -    { url = "https://files.pythonhosted.org/packages/af/85/a94e5cfaa0ca449d8f91c3d6f78313ebf919a0dbd55a100c711c6e9655bc/Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7", size = 2930206, upload-time = "2024-10-18T12:32:51.198Z" },
    -    { url = "https://files.pythonhosted.org/packages/c2/f0/a61d9262cd01351df22e57ad7c34f66794709acab13f34be2675f45bf89d/Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0", size = 333804, upload-time = "2024-10-18T12:32:52.661Z" },
    -    { url = "https://files.pythonhosted.org/packages/7e/c1/ec214e9c94000d1c1974ec67ced1c970c148aa6b8d8373066123fc3dbf06/Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b", size = 358517, upload-time = "2024-10-18T12:32:54.066Z" },
    -    { url = "https://files.pythonhosted.org/packages/1b/aa/aa6e0c9848ee4375514af0b27abf470904992939b7363ae78fc8aca8a9a8/Brotli-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5fb2ce4b8045c78ebbc7b8f3c15062e435d47e7393cc57c25115cfd49883747a", size = 873048, upload-time = "2023-09-07T14:05:21.205Z" },
    -    { url = "https://files.pythonhosted.org/packages/ae/32/38bba1a8bef9ecb1cda08439fd28d7e9c51aff13b4783a4f1610da90b6c2/Brotli-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7905193081db9bfa73b1219140b3d315831cbff0d8941f22da695832f0dd188f", size = 446207, upload-time = "2023-09-07T14:05:23.21Z" },
    -    { url = "https://files.pythonhosted.org/packages/3c/6a/14cc20ddc53efc274601c8195791a27cfb7acc5e5134e0f8c493a8b8821a/Brotli-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a77def80806c421b4b0af06f45d65a136e7ac0bdca3c09d9e2ea4e515367c7e9", size = 2903803, upload-time = "2023-09-07T14:05:24.864Z" },
    -    { url = "https://files.pythonhosted.org/packages/9a/26/62b2d894d4e82d7a7f4e0bb9007a42bbc765697a5679b43186acd68d7a79/Brotli-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dadd1314583ec0bf2d1379f7008ad627cd6336625d6679cf2f8e67081b83acf", size = 2941149, upload-time = "2023-09-07T14:05:26.479Z" },
    -    { url = "https://files.pythonhosted.org/packages/a9/ca/00d55bbdd8631236c61777742d8a8454cf6a87eb4125cad675912c68bec7/Brotli-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:901032ff242d479a0efa956d853d16875d42157f98951c0230f69e69f9c09bac", size = 2672253, upload-time = "2023-09-07T14:05:28.133Z" },
    -    { url = "https://files.pythonhosted.org/packages/e2/e6/4a730f6e5b5d538e92d09bc51bf69119914f29a222f9e1d65ae4abb27a4e/Brotli-1.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22fc2a8549ffe699bfba2256ab2ed0421a7b8fadff114a3d201794e45a9ff578", size = 2757005, upload-time = "2023-09-07T14:05:29.812Z" },
    -    { url = "https://files.pythonhosted.org/packages/cb/6b/8cf297987fe3c1bf1c87f0c0b714af2ce47092b8d307b9f6ecbc65f98968/Brotli-1.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ae15b066e5ad21366600ebec29a7ccbc86812ed267e4b28e860b8ca16a2bc474", size = 2910658, upload-time = "2023-09-07T14:05:31.376Z" },
    -    { url = "https://files.pythonhosted.org/packages/2c/1f/be9443995821c933aad7159803f84ef4923c6f5b72c2affd001192b310fc/Brotli-1.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:949f3b7c29912693cee0afcf09acd6ebc04c57af949d9bf77d6101ebb61e388c", size = 2809728, upload-time = "2023-09-07T14:05:32.923Z" },
    -    { url = "https://files.pythonhosted.org/packages/76/2f/213bab6efa902658c80a1247142d42b138a27ccdd6bade49ca9cd74e714a/Brotli-1.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:89f4988c7203739d48c6f806f1e87a1d96e0806d44f0fba61dba81392c9e474d", size = 2935043, upload-time = "2023-09-07T14:05:34.607Z" },
    -    { url = "https://files.pythonhosted.org/packages/27/89/bbb14fa98e895d1e601491fba54a5feec167d262f0d3d537a3b0d4cd0029/Brotli-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:de6551e370ef19f8de1807d0a9aa2cdfdce2e85ce88b122fe9f6b2b076837e59", size = 2930639, upload-time = "2023-09-07T14:05:36.317Z" },
    -    { url = "https://files.pythonhosted.org/packages/14/87/03a6d6e1866eddf9f58cc57e35befbeb5514da87a416befe820150cae63f/Brotli-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0737ddb3068957cf1b054899b0883830bb1fec522ec76b1098f9b6e0f02d9419", size = 2932834, upload-time = "2024-10-18T12:33:18.364Z" },
    -    { url = "https://files.pythonhosted.org/packages/a4/d5/e5f85e04f75144d1a89421ba432def6bdffc8f28b04f5b7d540bbd03362c/Brotli-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4f3607b129417e111e30637af1b56f24f7a49e64763253bbc275c75fa887d4b2", size = 2845213, upload-time = "2024-10-18T12:33:20.059Z" },
    -    { url = "https://files.pythonhosted.org/packages/99/bf/25ef07add7afbb1aacd4460726a1a40370dfd60c0810b6f242a6d3871d7e/Brotli-1.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:6c6e0c425f22c1c719c42670d561ad682f7bfeeef918edea971a79ac5252437f", size = 3031573, upload-time = "2024-10-18T12:33:22.541Z" },
    -    { url = "https://files.pythonhosted.org/packages/55/22/948a97bda5c9dc9968d56b9ed722d9727778db43739cf12ef26ff69be94d/Brotli-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:494994f807ba0b92092a163a0a283961369a65f6cbe01e8891132b7a320e61eb", size = 2926885, upload-time = "2024-10-18T12:33:24.781Z" },
    -    { url = "https://files.pythonhosted.org/packages/31/ba/e53d107399b535ef89deb6977dd8eae468e2dde7b1b74c6cbe2c0e31fda2/Brotli-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f0d8a7a6b5983c2496e364b969f0e526647a06b075d034f3297dc66f3b360c64", size = 333171, upload-time = "2023-09-07T14:05:38.071Z" },
    -    { url = "https://files.pythonhosted.org/packages/99/b3/f7b3af539f74b82e1c64d28685a5200c631cc14ae751d37d6ed819655627/Brotli-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:cdad5b9014d83ca68c25d2e9444e28e967ef16e80f6b436918c700c117a85467", size = 357258, upload-time = "2023-09-07T14:05:39.591Z" },
    +sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/64/10/a090475284fc4a71aed40a96f32e44a7fe5bda39687353dd977720b211b6/brotli-1.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3b90b767916ac44e93a8e28ce6adf8d551e43affb512f2377c732d486ac6514e", size = 863089, upload-time = "2025-11-05T18:38:01.181Z" },
    +    { url = "https://files.pythonhosted.org/packages/03/41/17416630e46c07ac21e378c3464815dd2e120b441e641bc516ac32cc51d2/brotli-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6be67c19e0b0c56365c6a76e393b932fb0e78b3b56b711d180dd7013cb1fd984", size = 445442, upload-time = "2025-11-05T18:38:02.434Z" },
    +    { url = "https://files.pythonhosted.org/packages/24/31/90cc06584deb5d4fcafc0985e37741fc6b9717926a78674bbb3ce018957e/brotli-1.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0bbd5b5ccd157ae7913750476d48099aaf507a79841c0d04a9db4415b14842de", size = 1532658, upload-time = "2025-11-05T18:38:03.588Z" },
    +    { url = "https://files.pythonhosted.org/packages/62/17/33bf0c83bcbc96756dfd712201d87342732fad70bb3472c27e833a44a4f9/brotli-1.2.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3f3c908bcc404c90c77d5a073e55271a0a498f4e0756e48127c35d91cf155947", size = 1631241, upload-time = "2025-11-05T18:38:04.582Z" },
    +    { url = "https://files.pythonhosted.org/packages/48/10/f47854a1917b62efe29bc98ac18e5d4f71df03f629184575b862ef2e743b/brotli-1.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1b557b29782a643420e08d75aea889462a4a8796e9a6cf5621ab05a3f7da8ef2", size = 1424307, upload-time = "2025-11-05T18:38:05.587Z" },
    +    { url = "https://files.pythonhosted.org/packages/e4/b7/f88eb461719259c17483484ea8456925ee057897f8e64487d76e24e5e38d/brotli-1.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81da1b229b1889f25adadc929aeb9dbc4e922bd18561b65b08dd9343cfccca84", size = 1488208, upload-time = "2025-11-05T18:38:06.613Z" },
    +    { url = "https://files.pythonhosted.org/packages/26/59/41bbcb983a0c48b0b8004203e74706c6b6e99a04f3c7ca6f4f41f364db50/brotli-1.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ff09cd8c5eec3b9d02d2408db41be150d8891c5566addce57513bf546e3d6c6d", size = 1597574, upload-time = "2025-11-05T18:38:07.838Z" },
    +    { url = "https://files.pythonhosted.org/packages/8e/e6/8c89c3bdabbe802febb4c5c6ca224a395e97913b5df0dff11b54f23c1788/brotli-1.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a1778532b978d2536e79c05dac2d8cd857f6c55cd0c95ace5b03740824e0e2f1", size = 1492109, upload-time = "2025-11-05T18:38:08.816Z" },
    +    { url = "https://files.pythonhosted.org/packages/ed/9a/4b19d4310b2dbd545c0c33f176b0528fa68c3cd0754e34b2f2bcf56548ae/brotli-1.2.0-cp310-cp310-win32.whl", hash = "sha256:b232029d100d393ae3c603c8ffd7e3fe6f798c5e28ddca5feabb8e8fdb732997", size = 334461, upload-time = "2025-11-05T18:38:10.729Z" },
    +    { url = "https://files.pythonhosted.org/packages/ac/39/70981d9f47705e3c2b95c0847dfa3e7a37aa3b7c6030aedc4873081ed005/brotli-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:ef87b8ab2704da227e83a246356a2b179ef826f550f794b2c52cddb4efbd0196", size = 369035, upload-time = "2025-11-05T18:38:11.827Z" },
    +    { url = "https://files.pythonhosted.org/packages/7a/ef/f285668811a9e1ddb47a18cb0b437d5fc2760d537a2fe8a57875ad6f8448/brotli-1.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:15b33fe93cedc4caaff8a0bd1eb7e3dab1c61bb22a0bf5bdfdfd97cd7da79744", size = 863110, upload-time = "2025-11-05T18:38:12.978Z" },
    +    { url = "https://files.pythonhosted.org/packages/50/62/a3b77593587010c789a9d6eaa527c79e0848b7b860402cc64bc0bc28a86c/brotli-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:898be2be399c221d2671d29eed26b6b2713a02c2119168ed914e7d00ceadb56f", size = 445438, upload-time = "2025-11-05T18:38:14.208Z" },
    +    { url = "https://files.pythonhosted.org/packages/cd/e1/7fadd47f40ce5549dc44493877db40292277db373da5053aff181656e16e/brotli-1.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:350c8348f0e76fff0a0fd6c26755d2653863279d086d3aa2c290a6a7251135dd", size = 1534420, upload-time = "2025-11-05T18:38:15.111Z" },
    +    { url = "https://files.pythonhosted.org/packages/12/8b/1ed2f64054a5a008a4ccd2f271dbba7a5fb1a3067a99f5ceadedd4c1d5a7/brotli-1.2.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1ad3fda65ae0d93fec742a128d72e145c9c7a99ee2fcd667785d99eb25a7fe", size = 1632619, upload-time = "2025-11-05T18:38:16.094Z" },
    +    { url = "https://files.pythonhosted.org/packages/89/5a/7071a621eb2d052d64efd5da2ef55ecdac7c3b0c6e4f9d519e9c66d987ef/brotli-1.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:40d918bce2b427a0c4ba189df7a006ac0c7277c180aee4617d99e9ccaaf59e6a", size = 1426014, upload-time = "2025-11-05T18:38:17.177Z" },
    +    { url = "https://files.pythonhosted.org/packages/26/6d/0971a8ea435af5156acaaccec1a505f981c9c80227633851f2810abd252a/brotli-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2a7f1d03727130fc875448b65b127a9ec5d06d19d0148e7554384229706f9d1b", size = 1489661, upload-time = "2025-11-05T18:38:18.41Z" },
    +    { url = "https://files.pythonhosted.org/packages/f3/75/c1baca8b4ec6c96a03ef8230fab2a785e35297632f402ebb1e78a1e39116/brotli-1.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9c79f57faa25d97900bfb119480806d783fba83cd09ee0b33c17623935b05fa3", size = 1599150, upload-time = "2025-11-05T18:38:19.792Z" },
    +    { url = "https://files.pythonhosted.org/packages/0d/1a/23fcfee1c324fd48a63d7ebf4bac3a4115bdb1b00e600f80f727d850b1ae/brotli-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:844a8ceb8483fefafc412f85c14f2aae2fb69567bf2a0de53cdb88b73e7c43ae", size = 1493505, upload-time = "2025-11-05T18:38:20.913Z" },
    +    { url = "https://files.pythonhosted.org/packages/36/e5/12904bbd36afeef53d45a84881a4810ae8810ad7e328a971ebbfd760a0b3/brotli-1.2.0-cp311-cp311-win32.whl", hash = "sha256:aa47441fa3026543513139cb8926a92a8e305ee9c71a6209ef7a97d91640ea03", size = 334451, upload-time = "2025-11-05T18:38:21.94Z" },
    +    { url = "https://files.pythonhosted.org/packages/02/8b/ecb5761b989629a4758c394b9301607a5880de61ee2ee5fe104b87149ebc/brotli-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:022426c9e99fd65d9475dce5c195526f04bb8be8907607e27e747893f6ee3e24", size = 369035, upload-time = "2025-11-05T18:38:22.941Z" },
    +    { url = "https://files.pythonhosted.org/packages/11/ee/b0a11ab2315c69bb9b45a2aaed022499c9c24a205c3a49c3513b541a7967/brotli-1.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:35d382625778834a7f3061b15423919aa03e4f5da34ac8e02c074e4b75ab4f84", size = 861543, upload-time = "2025-11-05T18:38:24.183Z" },
    +    { url = "https://files.pythonhosted.org/packages/e1/2f/29c1459513cd35828e25531ebfcbf3e92a5e49f560b1777a9af7203eb46e/brotli-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a61c06b334bd99bc5ae84f1eeb36bfe01400264b3c352f968c6e30a10f9d08b", size = 444288, upload-time = "2025-11-05T18:38:25.139Z" },
    +    { url = "https://files.pythonhosted.org/packages/3d/6f/feba03130d5fceadfa3a1bb102cb14650798c848b1df2a808356f939bb16/brotli-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acec55bb7c90f1dfc476126f9711a8e81c9af7fb617409a9ee2953115343f08d", size = 1528071, upload-time = "2025-11-05T18:38:26.081Z" },
    +    { url = "https://files.pythonhosted.org/packages/2b/38/f3abb554eee089bd15471057ba85f47e53a44a462cfce265d9bf7088eb09/brotli-1.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:260d3692396e1895c5034f204f0db022c056f9e2ac841593a4cf9426e2a3faca", size = 1626913, upload-time = "2025-11-05T18:38:27.284Z" },
    +    { url = "https://files.pythonhosted.org/packages/03/a7/03aa61fbc3c5cbf99b44d158665f9b0dd3d8059be16c460208d9e385c837/brotli-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:072e7624b1fc4d601036ab3f4f27942ef772887e876beff0301d261210bca97f", size = 1419762, upload-time = "2025-11-05T18:38:28.295Z" },
    +    { url = "https://files.pythonhosted.org/packages/21/1b/0374a89ee27d152a5069c356c96b93afd1b94eae83f1e004b57eb6ce2f10/brotli-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adedc4a67e15327dfdd04884873c6d5a01d3e3b6f61406f99b1ed4865a2f6d28", size = 1484494, upload-time = "2025-11-05T18:38:29.29Z" },
    +    { url = "https://files.pythonhosted.org/packages/cf/57/69d4fe84a67aef4f524dcd075c6eee868d7850e85bf01d778a857d8dbe0a/brotli-1.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7a47ce5c2288702e09dc22a44d0ee6152f2c7eda97b3c8482d826a1f3cfc7da7", size = 1593302, upload-time = "2025-11-05T18:38:30.639Z" },
    +    { url = "https://files.pythonhosted.org/packages/d5/3b/39e13ce78a8e9a621c5df3aeb5fd181fcc8caba8c48a194cd629771f6828/brotli-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:af43b8711a8264bb4e7d6d9a6d004c3a2019c04c01127a868709ec29962b6036", size = 1487913, upload-time = "2025-11-05T18:38:31.618Z" },
    +    { url = "https://files.pythonhosted.org/packages/62/28/4d00cb9bd76a6357a66fcd54b4b6d70288385584063f4b07884c1e7286ac/brotli-1.2.0-cp312-cp312-win32.whl", hash = "sha256:e99befa0b48f3cd293dafeacdd0d191804d105d279e0b387a32054c1180f3161", size = 334362, upload-time = "2025-11-05T18:38:32.939Z" },
    +    { url = "https://files.pythonhosted.org/packages/1c/4e/bc1dcac9498859d5e353c9b153627a3752868a9d5f05ce8dedd81a2354ab/brotli-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:b35c13ce241abdd44cb8ca70683f20c0c079728a36a996297adb5334adfc1c44", size = 369115, upload-time = "2025-11-05T18:38:33.765Z" },
    +    { url = "https://files.pythonhosted.org/packages/6c/d4/4ad5432ac98c73096159d9ce7ffeb82d151c2ac84adcc6168e476bb54674/brotli-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab", size = 861523, upload-time = "2025-11-05T18:38:34.67Z" },
    +    { url = "https://files.pythonhosted.org/packages/91/9f/9cc5bd03ee68a85dc4bc89114f7067c056a3c14b3d95f171918c088bf88d/brotli-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c", size = 444289, upload-time = "2025-11-05T18:38:35.6Z" },
    +    { url = "https://files.pythonhosted.org/packages/2e/b6/fe84227c56a865d16a6614e2c4722864b380cb14b13f3e6bef441e73a85a/brotli-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f", size = 1528076, upload-time = "2025-11-05T18:38:36.639Z" },
    +    { url = "https://files.pythonhosted.org/packages/55/de/de4ae0aaca06c790371cf6e7ee93a024f6b4bb0568727da8c3de112e726c/brotli-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6", size = 1626880, upload-time = "2025-11-05T18:38:37.623Z" },
    +    { url = "https://files.pythonhosted.org/packages/5f/16/a1b22cbea436642e071adcaf8d4b350a2ad02f5e0ad0da879a1be16188a0/brotli-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c", size = 1419737, upload-time = "2025-11-05T18:38:38.729Z" },
    +    { url = "https://files.pythonhosted.org/packages/46/63/c968a97cbb3bdbf7f974ef5a6ab467a2879b82afbc5ffb65b8acbb744f95/brotli-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48", size = 1484440, upload-time = "2025-11-05T18:38:39.916Z" },
    +    { url = "https://files.pythonhosted.org/packages/06/9d/102c67ea5c9fc171f423e8399e585dabea29b5bc79b05572891e70013cdd/brotli-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18", size = 1593313, upload-time = "2025-11-05T18:38:41.24Z" },
    +    { url = "https://files.pythonhosted.org/packages/9e/4a/9526d14fa6b87bc827ba1755a8440e214ff90de03095cacd78a64abe2b7d/brotli-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5", size = 1487945, upload-time = "2025-11-05T18:38:42.277Z" },
    +    { url = "https://files.pythonhosted.org/packages/5b/e8/3fe1ffed70cbef83c5236166acaed7bb9c766509b157854c80e2f766b38c/brotli-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1b1d6a4efedd53671c793be6dd760fcf2107da3a52331ad9ea429edf0902f27a", size = 334368, upload-time = "2025-11-05T18:38:43.345Z" },
    +    { url = "https://files.pythonhosted.org/packages/ff/91/e739587be970a113b37b821eae8097aac5a48e5f0eca438c22e4c7dd8648/brotli-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:b63daa43d82f0cdabf98dee215b375b4058cce72871fd07934f179885aad16e8", size = 369116, upload-time = "2025-11-05T18:38:44.609Z" },
    +    { url = "https://files.pythonhosted.org/packages/17/e1/298c2ddf786bb7347a1cd71d63a347a79e5712a7c0cba9e3c3458ebd976f/brotli-1.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21", size = 863080, upload-time = "2025-11-05T18:38:45.503Z" },
    +    { url = "https://files.pythonhosted.org/packages/84/0c/aac98e286ba66868b2b3b50338ffbd85a35c7122e9531a73a37a29763d38/brotli-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac", size = 445453, upload-time = "2025-11-05T18:38:46.433Z" },
    +    { url = "https://files.pythonhosted.org/packages/ec/f1/0ca1f3f99ae300372635ab3fe2f7a79fa335fee3d874fa7f9e68575e0e62/brotli-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e", size = 1528168, upload-time = "2025-11-05T18:38:47.371Z" },
    +    { url = "https://files.pythonhosted.org/packages/d6/a6/2ebfc8f766d46df8d3e65b880a2e220732395e6d7dc312c1e1244b0f074a/brotli-1.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7", size = 1627098, upload-time = "2025-11-05T18:38:48.385Z" },
    +    { url = "https://files.pythonhosted.org/packages/f3/2f/0976d5b097ff8a22163b10617f76b2557f15f0f39d6a0fe1f02b1a53e92b/brotli-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63", size = 1419861, upload-time = "2025-11-05T18:38:49.372Z" },
    +    { url = "https://files.pythonhosted.org/packages/9c/97/d76df7176a2ce7616ff94c1fb72d307c9a30d2189fe877f3dd99af00ea5a/brotli-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b", size = 1484594, upload-time = "2025-11-05T18:38:50.655Z" },
    +    { url = "https://files.pythonhosted.org/packages/d3/93/14cf0b1216f43df5609f5b272050b0abd219e0b54ea80b47cef9867b45e7/brotli-1.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361", size = 1593455, upload-time = "2025-11-05T18:38:51.624Z" },
    +    { url = "https://files.pythonhosted.org/packages/b3/73/3183c9e41ca755713bdf2cc1d0810df742c09484e2e1ddd693bee53877c1/brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888", size = 1488164, upload-time = "2025-11-05T18:38:53.079Z" },
    +    { url = "https://files.pythonhosted.org/packages/64/6a/0c78d8f3a582859236482fd9fa86a65a60328a00983006bcf6d83b7b2253/brotli-1.2.0-cp314-cp314-win32.whl", hash = "sha256:832c115a020e463c2f67664560449a7bea26b0c1fdd690352addad6d0a08714d", size = 339280, upload-time = "2025-11-05T18:38:54.02Z" },
    +    { url = "https://files.pythonhosted.org/packages/f5/10/56978295c14794b2c12007b07f3e41ba26acda9257457d7085b0bb3bb90c/brotli-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3", size = 375639, upload-time = "2025-11-05T18:38:55.67Z" },
    +    { url = "https://files.pythonhosted.org/packages/0f/1d/7787912f3fd30845d2927241bcd5aa2a9fde45b3e866394ee8155e49f612/brotli-1.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8d4f47f284bdd28629481c97b5f29ad67544fa258d9091a6ed1fda47c7347cd1", size = 862928, upload-time = "2025-11-05T18:39:31.398Z" },
    +    { url = "https://files.pythonhosted.org/packages/d8/29/663fd4195dbbd90aa118874dd67ca438ba0ac039d67902ff46c7105196f3/brotli-1.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2881416badd2a88a7a14d981c103a52a23a276a553a8aacc1346c2ff47c8dc17", size = 445365, upload-time = "2025-11-05T18:39:32.42Z" },
    +    { url = "https://files.pythonhosted.org/packages/96/14/d57282ff7da3e9238899c1bebb5f1d94265a1b76002f8a984ef5826d8ae8/brotli-1.2.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d39b54b968f4b49b5e845758e202b1035f948b0561ff5e6385e855c96625971", size = 1531224, upload-time = "2025-11-05T18:39:33.364Z" },
    +    { url = "https://files.pythonhosted.org/packages/25/1a/ea1b65a92e0e317306b8b207757c0e21376b14984cfd8d4c746a0efe7ed1/brotli-1.2.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95db242754c21a88a79e01504912e537808504465974ebb92931cfca2510469e", size = 1630502, upload-time = "2025-11-05T18:39:34.359Z" },
    +    { url = "https://files.pythonhosted.org/packages/6a/a4/68cd62219295ab8844731ebf64a5c60ba84358c62b130a5077ea90e2a73a/brotli-1.2.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bba6e7e6cfe1e6cb6eb0b7c2736a6059461de1fa2c0ad26cf845de6c078d16c8", size = 1423310, upload-time = "2025-11-05T18:39:35.717Z" },
    +    { url = "https://files.pythonhosted.org/packages/a1/1d/e0b2a429cbe50f673cb318debd42297525e08add574677cce78c99041747/brotli-1.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:88ef7d55b7bcf3331572634c3fd0ed327d237ceb9be6066810d39020a3ebac7a", size = 1487431, upload-time = "2025-11-05T18:39:37.149Z" },
    +    { url = "https://files.pythonhosted.org/packages/af/28/b8ddaf1b719818c22344f03ff2add71e387223408ea0a95f56f6ef8b8f5d/brotli-1.2.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:7fa18d65a213abcfbb2f6cafbb4c58863a8bd6f2103d65203c520ac117d1944b", size = 1596969, upload-time = "2025-11-05T18:39:38.395Z" },
    +    { url = "https://files.pythonhosted.org/packages/b8/a6/c790ef38cd49a9e27798a4b12681175f8c06cc76440e9deac22592fa7cd8/brotli-1.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:09ac247501d1909e9ee47d309be760c89c990defbb2e0240845c892ea5ff0de4", size = 1491229, upload-time = "2025-11-05T18:39:39.506Z" },
    +    { url = "https://files.pythonhosted.org/packages/3e/d3/c09cc2348d1c92845752967cedd881fa7865d270caeab9153453037a872b/brotli-1.2.0-cp39-cp39-win32.whl", hash = "sha256:c25332657dee6052ca470626f18349fc1fe8855a56218e19bd7a8c6ad4952c49", size = 334437, upload-time = "2025-11-05T18:39:40.534Z" },
    +    { url = "https://files.pythonhosted.org/packages/1b/df/e7c780e463ee7bd7951770692bbea5a605f56b9809ec7f6ce751d7b2ee88/brotli-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:1ce223652fd4ed3eb2b7f78fbea31c52314baecfac68db44037bb4167062a937", size = 369008, upload-time = "2025-11-05T18:39:41.515Z" },
     ]
     
     [[package]]
     name = "brotlicffi"
    -version = "1.1.0.0"
    +version = "1.2.0.0"
     source = { registry = "https://pypi.org/simple" }
     dependencies = [
         { name = "cffi" },
     ]
    -sdist = { url = "https://files.pythonhosted.org/packages/95/9d/70caa61192f570fcf0352766331b735afa931b4c6bc9a348a0925cc13288/brotlicffi-1.1.0.0.tar.gz", hash = "sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13", size = 465192, upload-time = "2023-09-14T14:22:40.707Z" }
    +sdist = { url = "https://files.pythonhosted.org/packages/84/85/57c314a6b35336efbbdc13e5fc9ae13f6b60a0647cfa7c1221178ac6d8ae/brotlicffi-1.2.0.0.tar.gz", hash = "sha256:34345d8d1f9d534fcac2249e57a4c3c8801a33c9942ff9f8574f67a175e17adb", size = 476682, upload-time = "2025-11-21T18:17:57.334Z" }
     wheels = [
    -    { url = "https://files.pythonhosted.org/packages/a2/11/7b96009d3dcc2c931e828ce1e157f03824a69fb728d06bfd7b2fc6f93718/brotlicffi-1.1.0.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9b7ae6bd1a3f0df532b6d67ff674099a96d22bc0948955cb338488c31bfb8851", size = 453786, upload-time = "2023-09-14T14:21:57.72Z" },
    -    { url = "https://files.pythonhosted.org/packages/d6/e6/a8f46f4a4ee7856fbd6ac0c6fb0dc65ed181ba46cd77875b8d9bbe494d9e/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19ffc919fa4fc6ace69286e0a23b3789b4219058313cf9b45625016bf7ff996b", size = 2911165, upload-time = "2023-09-14T14:21:59.613Z" },
    -    { url = "https://files.pythonhosted.org/packages/be/20/201559dff14e83ba345a5ec03335607e47467b6633c210607e693aefac40/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9feb210d932ffe7798ee62e6145d3a757eb6233aa9a4e7db78dd3690d7755814", size = 2927895, upload-time = "2023-09-14T14:22:01.22Z" },
    -    { url = "https://files.pythonhosted.org/packages/cd/15/695b1409264143be3c933f708a3f81d53c4a1e1ebbc06f46331decbf6563/brotlicffi-1.1.0.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84763dbdef5dd5c24b75597a77e1b30c66604725707565188ba54bab4f114820", size = 2851834, upload-time = "2023-09-14T14:22:03.571Z" },
    -    { url = "https://files.pythonhosted.org/packages/b4/40/b961a702463b6005baf952794c2e9e0099bde657d0d7e007f923883b907f/brotlicffi-1.1.0.0-cp37-abi3-win32.whl", hash = "sha256:1b12b50e07c3911e1efa3a8971543e7648100713d4e0971b13631cce22c587eb", size = 341731, upload-time = "2023-09-14T14:22:05.74Z" },
    -    { url = "https://files.pythonhosted.org/packages/1c/fa/5408a03c041114ceab628ce21766a4ea882aa6f6f0a800e04ee3a30ec6b9/brotlicffi-1.1.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:994a4f0681bb6c6c3b0925530a1926b7a189d878e6e5e38fae8efa47c5d9c613", size = 366783, upload-time = "2023-09-14T14:22:07.096Z" },
    -    { url = "https://files.pythonhosted.org/packages/e5/3b/bd4f3d2bcf2306ae66b0346f5b42af1962480b200096ffc7abc3bd130eca/brotlicffi-1.1.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2e4aeb0bd2540cb91b069dbdd54d458da8c4334ceaf2d25df2f4af576d6766ca", size = 397397, upload-time = "2023-09-14T14:22:08.519Z" },
    -    { url = "https://files.pythonhosted.org/packages/54/10/1fd57864449360852c535c2381ee7120ba8f390aa3869df967c44ca7eba1/brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b7b0033b0d37bb33009fb2fef73310e432e76f688af76c156b3594389d81391", size = 379698, upload-time = "2023-09-14T14:22:10.52Z" },
    -    { url = "https://files.pythonhosted.org/packages/e5/95/15aa422aa6450e6556e54a5fd1650ff59f470aed77ac739aa90ab63dc611/brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54a07bb2374a1eba8ebb52b6fafffa2afd3c4df85ddd38fcc0511f2bb387c2a8", size = 378635, upload-time = "2023-09-14T14:22:11.982Z" },
    -    { url = "https://files.pythonhosted.org/packages/6c/a7/f254e13b2cb43337d6d99a4ec10394c134e41bfda8a2eff15b75627f4a3d/brotlicffi-1.1.0.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7901a7dc4b88f1c1475de59ae9be59799db1007b7d059817948d8e4f12e24e35", size = 385719, upload-time = "2023-09-14T14:22:13.483Z" },
    -    { url = "https://files.pythonhosted.org/packages/72/a9/0971251c4427c14b2a827dba3d910d4d3330dabf23d4278bf6d06a978847/brotlicffi-1.1.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce01c7316aebc7fce59da734286148b1d1b9455f89cf2c8a4dfce7d41db55c2d", size = 361760, upload-time = "2023-09-14T14:22:14.767Z" },
    -    { url = "https://files.pythonhosted.org/packages/35/9b/e0b577351e1d9d5890e1a56900c4ceaaef783b807145cd229446a43cf437/brotlicffi-1.1.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:1a807d760763e398bbf2c6394ae9da5815901aa93ee0a37bca5efe78d4ee3171", size = 397392, upload-time = "2023-09-14T14:22:32.2Z" },
    -    { url = "https://files.pythonhosted.org/packages/4f/7f/a16534d28386f74781db8b4544a764cf955abae336379a76f50e745bb0ee/brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa8ca0623b26c94fccc3a1fdd895be1743b838f3917300506d04aa3346fd2a14", size = 379695, upload-time = "2023-09-14T14:22:33.85Z" },
    -    { url = "https://files.pythonhosted.org/packages/50/2a/699388b5e489726991132441b55aff0691dd73c49105ef220408a5ab98d6/brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3de0cf28a53a3238b252aca9fed1593e9d36c1d116748013339f0949bfc84112", size = 378629, upload-time = "2023-09-14T14:22:35.9Z" },
    -    { url = "https://files.pythonhosted.org/packages/4a/3f/58254e7fbe6011bf043e4dcade0e16995a9f82b731734fad97220d201f42/brotlicffi-1.1.0.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6be5ec0e88a4925c91f3dea2bb0013b3a2accda6f77238f76a34a1ea532a1cb0", size = 385712, upload-time = "2023-09-14T14:22:37.767Z" },
    -    { url = "https://files.pythonhosted.org/packages/40/16/2a29a625a6f74d13726387f83484dfaaf6fcdaafaadfbe26a0412ae268cc/brotlicffi-1.1.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d9eb71bb1085d996244439154387266fd23d6ad37161f6f52f1cd41dd95a3808", size = 361747, upload-time = "2023-09-14T14:22:39.368Z" },
    +    { url = "https://files.pythonhosted.org/packages/e4/df/a72b284d8c7bef0ed5756b41c2eb7d0219a1dd6ac6762f1c7bdbc31ef3af/brotlicffi-1.2.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:9458d08a7ccde8e3c0afedbf2c70a8263227a68dea5ab13590593f4c0a4fd5f4", size = 432340, upload-time = "2025-11-21T18:17:42.277Z" },
    +    { url = "https://files.pythonhosted.org/packages/74/2b/cc55a2d1d6fb4f5d458fba44a3d3f91fb4320aa14145799fd3a996af0686/brotlicffi-1.2.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:84e3d0020cf1bd8b8131f4a07819edee9f283721566fe044a20ec792ca8fd8b7", size = 1534002, upload-time = "2025-11-21T18:17:43.746Z" },
    +    { url = "https://files.pythonhosted.org/packages/e4/9c/d51486bf366fc7d6735f0e46b5b96ca58dc005b250263525a1eea3cd5d21/brotlicffi-1.2.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:33cfb408d0cff64cd50bef268c0fed397c46fbb53944aa37264148614a62e990", size = 1536547, upload-time = "2025-11-21T18:17:45.729Z" },
    +    { url = "https://files.pythonhosted.org/packages/1b/37/293a9a0a7caf17e6e657668bebb92dfe730305999fe8c0e2703b8888789c/brotlicffi-1.2.0.0-cp38-abi3-win32.whl", hash = "sha256:23e5c912fdc6fd37143203820230374d24babd078fc054e18070a647118158f6", size = 343085, upload-time = "2025-11-21T18:17:48.887Z" },
    +    { url = "https://files.pythonhosted.org/packages/07/6b/6e92009df3b8b7272f85a0992b306b61c34b7ea1c4776643746e61c380ac/brotlicffi-1.2.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:f139a7cdfe4ae7859513067b736eb44d19fae1186f9e99370092f6915216451b", size = 378586, upload-time = "2025-11-21T18:17:50.531Z" },
    +    { url = "https://files.pythonhosted.org/packages/a4/ec/52488a0563f1663e2ccc75834b470650f4b8bcdea3132aef3bf67219c661/brotlicffi-1.2.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fa102a60e50ddbd08de86a63431a722ea216d9bc903b000bf544149cc9b823dc", size = 402002, upload-time = "2025-11-21T18:17:51.76Z" },
    +    { url = "https://files.pythonhosted.org/packages/e4/63/d4aea4835fd97da1401d798d9b8ba77227974de565faea402f520b37b10f/brotlicffi-1.2.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d3c4332fc808a94e8c1035950a10d04b681b03ab585ce897ae2a360d479037c", size = 406447, upload-time = "2025-11-21T18:17:53.614Z" },
    +    { url = "https://files.pythonhosted.org/packages/62/4e/5554ecb2615ff035ef8678d4e419549a0f7a28b3f096b272174d656749fb/brotlicffi-1.2.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb4eb5830026b79a93bf503ad32b2c5257315e9ffc49e76b2715cffd07c8e3db", size = 402521, upload-time = "2025-11-21T18:17:54.875Z" },
    +    { url = "https://files.pythonhosted.org/packages/b5/d3/b07f8f125ac52bbee5dc00ef0d526f820f67321bf4184f915f17f50a4657/brotlicffi-1.2.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3832c66e00d6d82087f20a972b2fc03e21cd99ef22705225a6f8f418a9158ecc", size = 374730, upload-time = "2025-11-21T18:17:56.334Z" },
     ]
     
     [[package]]
    @@ -2444,8 +2419,8 @@ mypy = [
     [package.metadata]
     requires-dist = [
         { name = "backports-zstd", marker = "python_full_version < '3.14' and extra == 'zstd'", specifier = ">=1.0.0" },
    -    { name = "brotli", marker = "platform_python_implementation == 'CPython' and extra == 'brotli'", specifier = ">=1.0.9" },
    -    { name = "brotlicffi", marker = "platform_python_implementation != 'CPython' and extra == 'brotli'", specifier = ">=0.8.0" },
    +    { name = "brotli", marker = "platform_python_implementation == 'CPython' and extra == 'brotli'", specifier = ">=1.2.0" },
    +    { name = "brotlicffi", marker = "platform_python_implementation != 'CPython' and extra == 'brotli'", specifier = ">=1.2.0.0" },
         { name = "h2", marker = "extra == 'h2'", specifier = ">=4,<5" },
         { name = "pysocks", marker = "extra == 'socks'", specifier = ">=1.5.6,!=1.5.7,<2.0" },
     ]
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.