From 6b61466357dade8eab83adcbf2afedb78c70f3b5 Mon Sep 17 00:00:00 2001 From: Rusty Conover Date: Mon, 22 Jun 2026 14:07:40 -0400 Subject: [PATCH] test: migrate aioresponses -> aiointercept; lift aiohttp to >=3.14.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit aioresponses 0.7.8 (latest, unmaintained) breaks on aiohttp 3.14's required ClientResponse.stream_writer kwarg, which forced an aiohttp <3.14 cap. That cap held back a batch of client-side aiohttp security fixes (CVE-2026-34993, -47265, the -502xx and -542xx series — 11 Dependabot alerts), since every fix lands in 3.14.0/3.14.1. Migrate the aiohttp test mocking to aiointercept, which supports aiohttp 3.14 (it routes requests through a real localhost test server and patches the DNS resolver to intercept external hosts). Its registration surface is aioresponses-compatible (get/head/..., CallbackResult, callback(url, *, headers, ...)), so test bodies are largely unchanged. - tests/_aiomock.py: new sync adapter. aiointercept is an async context manager and the fetch tests are synchronous (fetch_url runs aiohttp on its own daemon thread), so mock_aiohttp() drives start()/stop() on a dedicated lifecycle thread — robust whether or not the caller already has a running event loop (covers the nested-event-loop test). Uses mock_external_urls=True so presigned URLs are intercepted. - tests/test_external_fetch.py / test_external.py / test_http.py / test_otel.py: swap imports to the adapter; exception= -> exception=True (aiointercept raises ClientConnectionError, still satisfying the existing ClientError / "Failed to resolve" assertions). - test_invalid_content_length_header: a real server can't emit a malformed Content-Length, so this now unit-tests _head_probe's defensive parse directly. - pyproject.toml: external extra aiohttp>=3.9,<3.14 -> aiohttp>=3.14.1; drop the aioresponses dev dep + its mypy override; add aiointercept (ships py.typed). - .github/dependabot.yml: drop the now-obsolete aiohttp >=3.14 ignore. Full suite: 3482 passed, 151 skipped; mypy/ruff/pydoclint/ty clean. Wall time improved (~129s -> ~99s vs the aioresponses baseline). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/dependabot.yml | 7 -- pyproject.toml | 14 ++-- tests/_aiomock.py | 88 +++++++++++++++++++++ tests/test_external.py | 50 ++++++------ tests/test_external_fetch.py | 118 ++++++++++++++++------------ tests/test_http.py | 26 +++---- tests/test_otel.py | 15 ++-- uv.lock | 144 +++++++++++++++++++---------------- 8 files changed, 287 insertions(+), 175 deletions(-) create mode 100644 tests/_aiomock.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6cc3002..c099227 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,13 +12,6 @@ updates: open-pull-requests-limit: 5 commit-message: prefix: "deps" - # Hold aiohttp below 3.14: it added a required ClientResponse.stream_writer - # kwarg that breaks aioresponses 0.7.8 (latest), failing the external/fetch - # test suites. Drop this ignore (and lift the pyproject cap) once - # aioresponses ships a 3.14-compatible release. - ignore: - - dependency-name: "aiohttp" - versions: [">=3.14"] # Roll all non-breaking bumps into a single weekly PR to keep noise down; # major bumps still open as individual PRs so they get reviewed on their own. groups: diff --git a/pyproject.toml b/pyproject.toml index 2da62fa..8dcc133 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,9 +29,11 @@ http = ["falcon>=3.0", "httpx>=0.24", "waitress>=2.0", "zstandard>=0.20", "pynac s3 = ["boto3>=1.28"] gcs = ["google-cloud-storage>=2.10"] cli = ["typer>=0.9", "httpx>=0.24", "filelock>=3.13"] -# aiohttp <3.14: 3.14 added a required ClientResponse.stream_writer kwarg that -# breaks aioresponses 0.7.8 (latest); lift the cap once aioresponses catches up. -external = ["aiohttp>=3.9,<3.14", "tenacity>=8.0", "zstandard>=0.20"] +# aiohttp >=3.14.1 picks up a batch of client-side security fixes (CVE-2026- +# 34993, -47265, -502xx, -542xx). The old <3.14 cap existed only because +# aioresponses broke on 3.14's ClientResponse.stream_writer kwarg; the test +# suite has since migrated to aiointercept, so the cap is lifted. +external = ["aiohttp>=3.14.1", "tenacity>=8.0", "zstandard>=0.20"] otel = ["opentelemetry-api>=1.20", "opentelemetry-sdk>=1.20"] oauth = ["joserfc>=1.0"] mtls = ["cryptography>=41.0"] @@ -102,10 +104,6 @@ ignore_missing_imports = true module = "moto.*" ignore_missing_imports = true -[[tool.mypy.overrides]] -module = "aioresponses.*" -ignore_missing_imports = true - [[tool.mypy.overrides]] module = "google.cloud.*" ignore_missing_imports = true @@ -207,7 +205,7 @@ docs = [ "pymdown-extensions>=10.21.2", ] dev = [ - "aioresponses>=0.7", + "aiointercept>=0.1.7", "joserfc>=1.0", "boto3>=1.28", "falcon>=3.0", diff --git a/tests/_aiomock.py b/tests/_aiomock.py new file mode 100644 index 0000000..242637c --- /dev/null +++ b/tests/_aiomock.py @@ -0,0 +1,88 @@ +# © Copyright 2025-2026, Query.Farm LLC - https://query.farm +# SPDX-License-Identifier: Apache-2.0 + +"""Synchronous adapter around :mod:`aiointercept` for the aiohttp test suites. + +The fetch/external tests are synchronous (``def test_...``) and drive +:func:`vgi_rpc.external_fetch.fetch_url`, which runs aiohttp on its own daemon +thread and event loop. :mod:`aiointercept` is an *async* context manager that +boots a real localhost test server and patches aiohttp's DNS resolver at the +class level so requests to any registered host are transparently intercepted +(``mock_external_urls=True``) — exactly what is needed to mock the presigned +URLs the fetcher downloads. + +:func:`mock_aiohttp` bridges the two: it drives ``aiointercept``'s async +``start``/``stop`` from synchronous test code on a transient event loop. The +class-level patches it installs are global, so they intercept the fetcher's +daemon-thread requests too. Registered callbacks are synchronous here, so the +transient loop used for startup never needs to outlive ``start()``. + +This replaces ``aioresponses`` (which broke on aiohttp >=3.14); the registration +surface (``get``/``head``/``post``/...; ``CallbackResult``; ``callback(url, *, +headers, query, json, data)``) is API-compatible, so test bodies are unchanged. +""" + +from __future__ import annotations + +import asyncio +import threading +from collections.abc import Coroutine, Iterator +from contextlib import contextmanager +from typing import Any + +from aiointercept import CallbackResult, aiointercept + +__all__ = ["CallbackResult", "aiointercept", "mock_aiohttp"] + + +def _run_sync(coro: Coroutine[Any, Any, None]) -> None: + """Run *coro* to completion on a throwaway loop in a dedicated thread. + + A dedicated thread keeps this safe whether or not the *calling* thread + already has a running event loop — some tests drive ``fetch_url`` from + inside their own loop (e.g. the nested-event-loop case), where a plain + :func:`asyncio.run` would raise ``RuntimeError``. ``aiointercept`` runs its + test server on its own daemon thread, so this loop only needs to live long + enough to execute ``start``/``stop``; the registered callbacks here are + synchronous and never reach back to it. + + Args: + coro: The ``start()`` or ``stop()`` coroutine to drive. + + """ + error: list[BaseException] = [] + + def _target() -> None: + loop = asyncio.new_event_loop() + try: + loop.run_until_complete(coro) + except BaseException as exc: # surface failures to the caller thread + error.append(exc) + finally: + loop.close() + + thread = threading.Thread(target=_target, name="aiomock-lifecycle") + thread.start() + thread.join() + if error: + raise error[0] + + +@contextmanager +def mock_aiohttp() -> Iterator[aiointercept]: + """Yield an :class:`aiointercept.aiointercept` mock, driven synchronously. + + Intercepts requests to any registered host (``mock_external_urls=True``) so + the daemon-thread fetcher's HTTPS requests to presigned URLs are mocked. + + Yields: + The started ``aiointercept`` instance; register handlers on it with + ``.get()`` / ``.head()`` / ``.post()`` and friends. + + """ + mock = aiointercept(mock_external_urls=True) + _run_sync(mock.start()) + try: + yield mock + finally: + _run_sync(mock.stop()) diff --git a/tests/test_external.py b/tests/test_external.py index e6c02a6..4718bbd 100644 --- a/tests/test_external.py +++ b/tests/test_external.py @@ -19,10 +19,10 @@ import pyarrow.compute as pc import pytest import zstandard -from aioresponses import CallbackResult -from aioresponses import aioresponses as aioresponses_ctx from pyarrow import ipc +from tests._aiomock import CallbackResult, aiointercept +from tests._aiomock import mock_aiohttp as aiointercept_ctx from vgi_rpc.external import ( Compression, ExternalLocationConfig, @@ -66,7 +66,7 @@ class MockStorage(ExternalStorage): """In-memory ExternalStorage for testing. - Uses ``https://`` URLs so aioresponses can intercept them. + Uses ``https://`` URLs so aiointercept can intercept them. """ def __init__(self) -> None: @@ -95,10 +95,10 @@ def generate_upload_url(self, schema: pa.Schema) -> UploadUrl: @contextmanager -def _mock_aio(storage: MockStorage, *, content_encoding: str | None = None) -> Iterator[aioresponses_ctx]: - """Context manager that registers all MockStorage URLs in aioresponses.""" +def _mock_aio(storage: MockStorage, *, content_encoding: str | None = None) -> Iterator[aiointercept]: + """Context manager that registers all MockStorage URLs in aiointercept.""" enc = content_encoding or storage.last_content_encoding - with aioresponses_ctx() as mock: + with aiointercept_ctx() as mock: for url, body in storage.data.items(): head_headers: dict[str, str] = {"Content-Length": str(len(body))} get_headers: dict[str, str] = {"Content-Length": str(len(body))} @@ -352,7 +352,7 @@ def test_retry_success(self) -> None: pointer, cm = make_external_location_batch(_SCHEMA, url) - with aioresponses_ctx() as mock: + with aiointercept_ctx() as mock: # First attempt: HEAD ok, GET fails; second attempt: HEAD ok, GET succeeds mock.head(url, headers={"Content-Length": str(len(ipc_bytes))}) mock.get(url, exception=aiohttp.ClientConnectionError("transient failure")) @@ -371,7 +371,7 @@ def test_all_retries_fail(self) -> None: pointer, cm = make_external_location_batch(_SCHEMA, url) with ( - aioresponses_ctx() as mock, + aiointercept_ctx() as mock, pytest.raises(RuntimeError, match="Failed to resolve"), ): # Both attempts: HEAD fails with connection error @@ -1039,7 +1039,7 @@ def bidi_large(self, factor: float) -> Stream[_LargeBidiState]: ) -def _mock_aio_dynamic(storage: MockStorage, mock: aioresponses_ctx, *, content_encoding: str | None = None) -> None: +def _mock_aio_dynamic(storage: MockStorage, mock: aiointercept, *, content_encoding: str | None = None) -> None: """Register pattern-based HEAD + GET callbacks that serve from MockStorage dynamically.""" pattern = re.compile(r"^https://mock\.storage/.*$") enc = content_encoding or storage.last_content_encoding @@ -1087,7 +1087,7 @@ def test_unary_large_externalized(self) -> None: storage = MockStorage() config = self._make_config(storage, threshold=10) - with aioresponses_ctx() as mock: + with aiointercept_ctx() as mock: _mock_aio_dynamic(storage, mock) with serve_pipe( _ExternalService, @@ -1121,7 +1121,7 @@ def test_server_stream_large_externalized(self) -> None: received_logs: list[Message] = [] - with aioresponses_ctx() as mock: + with aiointercept_ctx() as mock: _mock_aio_dynamic(storage, mock) with serve_pipe( _ExternalService, @@ -1145,7 +1145,7 @@ def test_server_stream_logs_from_external(self) -> None: received_logs: list[Message] = [] - with aioresponses_ctx() as mock: + with aiointercept_ctx() as mock: _mock_aio_dynamic(storage, mock) with serve_pipe( _ExternalService, @@ -1165,7 +1165,7 @@ def test_bidi_large_output_externalized(self) -> None: storage = MockStorage() config = self._make_config(storage, threshold=100) - with aioresponses_ctx() as mock: + with aiointercept_ctx() as mock: _mock_aio_dynamic(storage, mock) with serve_pipe( _ExternalService, @@ -1189,7 +1189,7 @@ def test_bidi_large_input_externalized(self) -> None: storage = MockStorage() config = self._make_config(storage, threshold=100) - with aioresponses_ctx() as mock: + with aiointercept_ctx() as mock: _mock_aio_dynamic(storage, mock) with serve_pipe( _ExternalService, @@ -1328,7 +1328,7 @@ def test_bidi_storage_none(self) -> None: class TestS3Storage: """Tests for S3Storage with moto mock. - Uses moto's mock_aws for S3 operations and aioresponses to intercept + Uses moto's mock_aws for S3 operations and aiointercept to intercept fetches, reading back from moto's mock S3. """ @@ -1373,7 +1373,7 @@ def test_full_roundtrip(self, _s3_env: tuple[Any, Any]) -> None: externalize_threshold_bytes=10, ) - # Helper to read objects from moto's mock S3 for aioresponses + # Helper to read objects from moto's mock S3 for aiointercept def _s3_callback(url_: Any, **kwargs: Any) -> CallbackResult: parsed = urlparse(str(url_)) key = parsed.path.lstrip("/") @@ -1388,7 +1388,7 @@ def _s3_head_callback(url_: Any, **kwargs: Any) -> CallbackResult: body: bytes = resp["Body"].read() return CallbackResult(status=200, headers={"Content-Length": str(len(body))}) - with aioresponses_ctx() as mock: + with aiointercept_ctx() as mock: pattern = re.compile(r"^https://.*test-bucket.*$") for _ in range(10): mock.head(pattern, callback=_s3_head_callback) @@ -1485,7 +1485,7 @@ def test_upload_and_signed_url(self, _gcs_mocks: tuple[MagicMock, MagicMock, Mag assert call_kwargs.kwargs["method"] == "GET" def test_full_roundtrip(self, _gcs_mocks: tuple[MagicMock, MagicMock, MagicMock, MagicMock]) -> None: - """Full round-trip with mocked GCS + aioresponses.""" + """Full round-trip with mocked GCS + aiointercept.""" from vgi_rpc.gcs import GCSStorage _, _, _, mock_blob = _gcs_mocks @@ -1507,7 +1507,7 @@ def capture_upload(data: bytes, content_type: str = "") -> None: assert ext_batch.num_rows == 0 assert ext_cm is not None - with aioresponses_ctx() as mock: + with aiointercept_ctx() as mock: body = uploaded["data"] mock.head( "https://storage.googleapis.com/test", @@ -1620,7 +1620,7 @@ def test_basic_resolution_with_fetch_config(self) -> None: pointer, cm = make_external_location_batch(_SCHEMA, url) - with aioresponses_ctx() as mock: + with aiointercept_ctx() as mock: mock.head(url, headers={"Content-Length": str(len(ipc_bytes))}) mock.get(url, body=ipc_bytes, headers={"Content-Length": str(len(ipc_bytes))}) @@ -1669,7 +1669,7 @@ def _range_callback(url_: Any, **kwargs: Any) -> CallbackResult: ) return CallbackResult(status=200, body=ipc_bytes) - with aioresponses_ctx() as mock: + with aiointercept_ctx() as mock: mock.head( url, headers={"Content-Length": str(len(ipc_bytes)), "Accept-Ranges": "bytes"}, @@ -1858,7 +1858,7 @@ def test_unary_large_compressed(self) -> None: storage = MockStorage() config = self._make_config(storage, threshold=10) - with aioresponses_ctx() as mock: + with aiointercept_ctx() as mock: _mock_aio_dynamic(storage, mock, content_encoding="zstd") with serve_pipe( _ExternalService, @@ -1880,7 +1880,7 @@ def test_server_stream_compressed(self) -> None: received_logs: list[Message] = [] - with aioresponses_ctx() as mock: + with aiointercept_ctx() as mock: _mock_aio_dynamic(storage, mock, content_encoding="zstd") with serve_pipe( _ExternalService, @@ -1900,7 +1900,7 @@ def test_bidi_compressed(self) -> None: storage = MockStorage() config = self._make_config(storage, threshold=100) - with aioresponses_ctx() as mock: + with aiointercept_ctx() as mock: _mock_aio_dynamic(storage, mock, content_encoding="zstd") with serve_pipe( _ExternalService, @@ -1984,7 +1984,7 @@ def test_large_header_externalized(self) -> None: storage = MockStorage() config = self._make_config(storage, threshold=10) - with aioresponses_ctx() as mock: + with aiointercept_ctx() as mock: _mock_aio_dynamic(storage, mock) with serve_pipe( _HeaderExternalService, diff --git a/tests/test_external_fetch.py b/tests/test_external_fetch.py index 6d94f7a..f36dcac 100644 --- a/tests/test_external_fetch.py +++ b/tests/test_external_fetch.py @@ -7,14 +7,14 @@ import asyncio import re -from typing import Any +from typing import Any, ClassVar from unittest.mock import AsyncMock, patch import aiohttp import pytest import zstandard -from aioresponses import CallbackResult, aioresponses +from tests._aiomock import CallbackResult, aiointercept, mock_aiohttp from vgi_rpc.external_fetch import FetchConfig, _compute_ranges, _ensure_pool, _head_probe, _reset_session, fetch_url # --------------------------------------------------------------------------- @@ -22,7 +22,7 @@ # --------------------------------------------------------------------------- -def _register_head(mock: aioresponses, url: str, data: bytes, *, accept_ranges: bool = True) -> None: +def _register_head(mock: aiointercept, url: str, data: bytes, *, accept_ranges: bool = True) -> None: """Register a HEAD handler that returns Content-Length and optionally Accept-Ranges.""" head_headers: dict[str, str] = {"Content-Length": str(len(data))} if accept_ranges: @@ -30,7 +30,7 @@ def _register_head(mock: aioresponses, url: str, data: bytes, *, accept_ranges: mock.head(url, headers=head_headers) -def _register_range_url(mock: aioresponses, url: str, data: bytes, *, accept_ranges: bool = True) -> None: +def _register_range_url(mock: aiointercept, url: str, data: bytes, *, accept_ranges: bool = True) -> None: """Register HEAD + GET (simple and Range) handlers for a URL. The HEAD response includes ``Content-Length`` and optionally @@ -38,7 +38,7 @@ def _register_range_url(mock: aioresponses, url: str, data: bytes, *, accept_ran full body or individual Range chunks. Args: - mock: The aioresponses mock instance. + mock: The aiointercept mock instance. url: The URL to register. data: The complete data the URL serves. accept_ranges: Whether to advertise Range support. @@ -115,7 +115,7 @@ def test_small_below_threshold(self) -> None: data = b"hello world" url = "https://example.com/small" with FetchConfig(parallel_threshold_bytes=1024) as config: - with aioresponses() as mock: + with mock_aiohttp() as mock: _register_head(mock, url, data) mock.get(url, body=data, headers={"Content-Length": str(len(data))}) @@ -128,7 +128,7 @@ def test_no_accept_ranges_fallback(self) -> None: data = b"x" * 100 url = "https://example.com/no-range" with FetchConfig(parallel_threshold_bytes=50) as config: - with aioresponses() as mock: + with mock_aiohttp() as mock: _register_head(mock, url, data, accept_ranges=False) mock.get(url, body=data, headers={"Content-Length": str(len(data))}) @@ -141,7 +141,7 @@ def test_no_content_length_fallback(self) -> None: data = b"streaming data" url = "https://example.com/no-cl" with FetchConfig(parallel_threshold_bytes=10) as config: - with aioresponses() as mock: + with mock_aiohttp() as mock: # HEAD returns no Content-Length mock.head(url, headers={"Accept-Ranges": "bytes"}) mock.get(url, body=data) @@ -159,7 +159,7 @@ def test_head_405_falls_back_to_get(self) -> None: data = b"head not allowed" url = "https://example.com/no-head" with FetchConfig(parallel_threshold_bytes=10000) as config: - with aioresponses() as mock: + with mock_aiohttp() as mock: mock.head(url, status=405) mock.get(url, body=data, headers={"Content-Length": str(len(data))}) @@ -172,7 +172,7 @@ def test_head_501_falls_back_to_get(self) -> None: data = b"head not implemented" url = "https://example.com/no-head-501" with FetchConfig(parallel_threshold_bytes=10000) as config: - with aioresponses() as mock: + with mock_aiohttp() as mock: mock.head(url, status=501) mock.get(url, body=data, headers={"Content-Length": str(len(data))}) @@ -189,7 +189,7 @@ def test_head_success_enables_parallel(self) -> None: chunk_size_bytes=512, max_parallel_requests=4, ) as config: - with aioresponses() as mock: + with mock_aiohttp() as mock: _register_range_url(mock, url, data) result = fetch_url(url, config) @@ -207,7 +207,7 @@ def test_head_403_presigned_falls_back_to_get(self) -> None: assert pool.loop is not None assert pool.session is not None - with aioresponses() as mock: + with mock_aiohttp() as mock: mock.head(url, status=403) future = asyncio.run_coroutine_threadsafe( @@ -252,7 +252,7 @@ def _probe_then_get(url_: Any, **kwargs: Any) -> CallbackResult: return CallbackResult(status=200, body=data, headers={"Content-Length": str(len(data))}) with FetchConfig(parallel_threshold_bytes=10_000) as config: - with aioresponses() as mock: + with mock_aiohttp() as mock: # No HEAD mock on purpose: presigned path should never issue HEAD. mock.get(url, callback=_probe_then_get, repeat=True) result = fetch_url(url, config) @@ -280,7 +280,7 @@ def _range_ignored(url_: Any, **kwargs: Any) -> CallbackResult: return CallbackResult(status=200, body=data, headers={"Content-Length": str(len(data))}) with FetchConfig(parallel_threshold_bytes=10_000) as config: - with aioresponses() as mock: + with mock_aiohttp() as mock: mock.get(url, callback=_range_ignored, repeat=True) result = fetch_url(url, config) @@ -307,7 +307,7 @@ def _probe_forbidden(url_: Any, **kwargs: Any) -> CallbackResult: return CallbackResult(status=200, body=data, headers={"Content-Length": str(len(data))}) with FetchConfig(parallel_threshold_bytes=10_000) as config: - with aioresponses() as mock: + with mock_aiohttp() as mock: mock.get(url, callback=_probe_forbidden, repeat=True) result = fetch_url(url, config) @@ -327,7 +327,7 @@ def test_large_parallel_chunks(self) -> None: chunk_size_bytes=2048, max_parallel_requests=4, ) as config: - with aioresponses() as mock: + with mock_aiohttp() as mock: _register_range_url(mock, url, data) result = fetch_url(url, config) @@ -347,7 +347,7 @@ def test_chunk_reassembly_order(self) -> None: chunk_size_bytes=chunk_size, max_parallel_requests=8, ) as config: - with aioresponses() as mock: + with mock_aiohttp() as mock: _register_range_url(mock, url, data) result = fetch_url(url, config) @@ -364,7 +364,7 @@ class TestFetchMaxBytes: def test_max_fetch_bytes_rejected_at_probe(self) -> None: """Content-Length exceeds limit → RuntimeError before download.""" url = "https://example.com/huge" - with FetchConfig(max_fetch_bytes=100) as config, aioresponses() as mock: + with FetchConfig(max_fetch_bytes=100) as config, mock_aiohttp() as mock: mock.head( url, headers={"Content-Length": "99999", "Accept-Ranges": "bytes"}, @@ -400,7 +400,7 @@ def _lying_callback(url_: Any, **kwargs: Any) -> CallbackResult: ) return CallbackResult(status=200, body=b"x" * 200) - with aioresponses() as mock: + with mock_aiohttp() as mock: mock.head(url, headers={"Content-Length": "200", "Accept-Ranges": "bytes"}) for _ in range(50): mock.get(url, callback=_lying_callback) @@ -426,7 +426,7 @@ def _ignore_range_callback(url_: Any, **kwargs: Any) -> CallbackResult: # Server ignores Range and returns full body with 200 return CallbackResult(status=200, body=data, headers={"Content-Length": str(len(data))}) - with aioresponses() as mock: + with mock_aiohttp() as mock: _register_head(mock, url, data) for _ in range(10): mock.get(url, callback=_ignore_range_callback) @@ -448,7 +448,7 @@ def test_hedging_disabled(self) -> None: max_parallel_requests=4, speculative_retry_multiplier=0, ) as config: - with aioresponses() as mock: + with mock_aiohttp() as mock: _register_range_url(mock, url, data) result = fetch_url(url, config) @@ -464,7 +464,7 @@ def test_multiple_chunks_hedged(self) -> None: max_parallel_requests=8, speculative_retry_multiplier=1.0, # aggressive — hedge anything above median ) as config: - with aioresponses() as mock: + with mock_aiohttp() as mock: _register_range_url(mock, url, data) result = fetch_url(url, config) @@ -473,7 +473,7 @@ def test_multiple_chunks_hedged(self) -> None: def test_slow_chunks_trigger_hedging(self) -> None: """Slow responses trigger time-based hedging; result is correct. - Uses ``repeat=True`` on a single mock to avoid an aioresponses + Uses ``repeat=True`` on a single mock to avoid an aiointercept ``KeyError`` race where two concurrent async callbacks (original + hedge) try to delete the same non-repeating mock entry. The sleep is generous (0.5 s) so that even on Windows — where asyncio timer @@ -516,7 +516,7 @@ async def _slow_callback(url_: Any, **kwargs: Any) -> CallbackResult: ) return CallbackResult(status=200, body=data) - with aioresponses() as mock: + with mock_aiohttp() as mock: _register_head(mock, url, data) mock.get(url, callback=_slow_callback, repeat=True) result = fetch_url(url, config) @@ -567,7 +567,7 @@ async def _slow_callback(url_: Any, **kwargs: Any) -> CallbackResult: ) return CallbackResult(status=200, body=data) - with aioresponses() as mock: + with mock_aiohttp() as mock: _register_head(mock, url, data) mock.get(url, callback=_slow_callback, repeat=True) result = fetch_url(url, config) @@ -587,7 +587,7 @@ def test_hedge_cap_zero_means_unlimited(self) -> None: speculative_retry_multiplier=1.0, max_speculative_hedges=0, ) as config: - with aioresponses() as mock: + with mock_aiohttp() as mock: _register_range_url(mock, url, data) result = fetch_url(url, config) @@ -646,7 +646,7 @@ async def _callback(url_: Any, **kwargs: Any) -> CallbackResult: }, ) - with aioresponses() as mock: + with mock_aiohttp() as mock: _register_head(mock, url, data) mock.get(url, callback=_callback, repeat=True) result = fetch_url(url, config) @@ -661,7 +661,7 @@ class TestFetchErrors: def test_server_error_propagates(self) -> None: """500 on HEAD raises aiohttp.ClientResponseError.""" url = "https://example.com/error" - with FetchConfig(parallel_threshold_bytes=10000) as config, aioresponses() as mock: + with FetchConfig(parallel_threshold_bytes=10000) as config, mock_aiohttp() as mock: mock.head(url, status=500) with pytest.raises(aiohttp.ClientResponseError): @@ -670,8 +670,8 @@ def test_server_error_propagates(self) -> None: def test_overall_timeout(self) -> None: """Overall deadline exceeded raises.""" url = "https://example.com/slow" - with FetchConfig(timeout_seconds=0.1, parallel_threshold_bytes=10000) as config, aioresponses() as mock: - mock.head(url, exception=TimeoutError()) + with FetchConfig(timeout_seconds=0.1, parallel_threshold_bytes=10000) as config, mock_aiohttp() as mock: + mock.head(url, exception=True) with pytest.raises((TimeoutError, aiohttp.ClientError)): fetch_url(url, config) @@ -682,7 +682,7 @@ def test_simple_path_exceeds_max_fetch_bytes(self) -> None: data = b"x" * 200 # HEAD returns no Content-Length so the header guard doesn't trigger — forces # the streaming _read_response_body path to catch the oversize body. - with FetchConfig(max_fetch_bytes=100, parallel_threshold_bytes=10000) as config, aioresponses() as mock: + with FetchConfig(max_fetch_bytes=100, parallel_threshold_bytes=10000) as config, mock_aiohttp() as mock: mock.head(url, headers={}) mock.get(url, body=data) @@ -690,17 +690,35 @@ def test_simple_path_exceeds_max_fetch_bytes(self) -> None: fetch_url(url, config) def test_invalid_content_length_header(self) -> None: - """Non-numeric Content-Length from HEAD falls back to simple GET.""" - data = b"some data" - url = "https://example.com/bad-cl" - with FetchConfig(parallel_threshold_bytes=5) as config: - with aioresponses() as mock: - mock.head(url, headers={"Content-Length": "not-a-number"}) - mock.get(url, body=data) + """Non-numeric Content-Length from HEAD is treated as unknown length. - result = fetch_url(url, config) + A real HTTP server can never emit a malformed ``Content-Length`` (and + aiointercept routes through one, so it can't be mocked end-to-end), but + ``_head_probe`` still guards the parse defensively. Exercise that branch + directly: a non-numeric header must yield ``content_length=None`` rather + than raising, which lets the caller fall back to a plain GET. + """ - assert result == data + class _Resp: + status = 200 + headers: ClassVar[dict[str, str]] = {"Content-Length": "not-a-number", "Accept-Ranges": "bytes"} + + def raise_for_status(self) -> None: + pass + + class _HeadCtx: + async def __aenter__(self) -> _Resp: + return _Resp() + + async def __aexit__(self, *exc: object) -> bool: + return False + + client = AsyncMock() + client.head = lambda _url: _HeadCtx() + + content_length, accept_ranges, _ = asyncio.run(_head_probe("https://example.com/bad-cl", client)) + assert content_length is None + assert accept_ranges == "bytes" def test_chunk_error_cancels_pending(self) -> None: """Error in a Range chunk cancels remaining tasks and propagates.""" @@ -729,7 +747,7 @@ def _fail_callback(url_: Any, **kwargs: Any) -> CallbackResult: ) return CallbackResult(status=200, body=b"") - with aioresponses() as mock: + with mock_aiohttp() as mock: mock.head(url, headers={"Content-Length": "300", "Accept-Ranges": "bytes"}) for _ in range(50): mock.get(url, callback=_fail_callback) @@ -748,7 +766,7 @@ def test_nested_event_loop(self) -> None: with FetchConfig(parallel_threshold_bytes=10000) as config: async def run_in_loop() -> bytes: - with aioresponses() as mock: + with mock_aiohttp() as mock: _register_head(mock, url, data, accept_ranges=False) mock.get(url, body=data, headers={"Content-Length": str(len(data))}) return fetch_url(url, config) @@ -770,7 +788,7 @@ def test_session_reused_across_calls(self) -> None: data = b"reuse me" url = "https://example.com/pool-reuse" with FetchConfig(parallel_threshold_bytes=10000) as config: - with aioresponses() as mock: + with mock_aiohttp() as mock: _register_head(mock, url, data, accept_ranges=False) mock.get(url, body=data, headers={"Content-Length": str(len(data))}) _register_head(mock, url, data, accept_ranges=False) @@ -829,7 +847,7 @@ def test_pool_recovers_after_close(self) -> None: assert config._pool.session is None # Use again — should re-initialize - with aioresponses() as mock: + with mock_aiohttp() as mock: _register_head(mock, url, data, accept_ranges=False) mock.get(url, body=data, headers={"Content-Length": str(len(data))}) result = fetch_url(url, config) @@ -910,7 +928,7 @@ def test_fetch_zstd_simple_path(self) -> None: compressed = zstandard.ZstdCompressor().compress(raw_data) url = "https://example.com/zstd-simple" with FetchConfig(parallel_threshold_bytes=10_000_000) as config: - with aioresponses() as mock: + with mock_aiohttp() as mock: mock.head( url, headers={ @@ -941,7 +959,7 @@ def test_fetch_zstd_parallel_path(self) -> None: chunk_size_bytes=512, max_parallel_requests=4, ) as config: - with aioresponses() as mock: + with mock_aiohttp() as mock: mock.head( url, headers={ @@ -981,7 +999,7 @@ def test_fetch_no_encoding_passthrough(self) -> None: data = b"raw bytes no compression" url = "https://example.com/no-encoding" with FetchConfig(parallel_threshold_bytes=10_000_000) as config: - with aioresponses() as mock: + with mock_aiohttp() as mock: mock.head( url, headers={"Content-Length": str(len(data))}, @@ -1004,7 +1022,7 @@ def test_head_probe_returns_content_encoding(self) -> None: assert pool.loop is not None assert pool.session is not None - with aioresponses() as mock: + with mock_aiohttp() as mock: mock.head( url, headers={ @@ -1030,7 +1048,7 @@ def test_corrupt_zstd_raises_runtime_error(self) -> None: """Corrupted zstd data with Content-Encoding: zstd raises RuntimeError.""" corrupt_data = b"this is not valid zstd data" url = "https://example.com/corrupt-zstd" - with FetchConfig(parallel_threshold_bytes=10_000_000) as config, aioresponses() as mock: + with FetchConfig(parallel_threshold_bytes=10_000_000) as config, mock_aiohttp() as mock: mock.head( url, headers={ @@ -1073,7 +1091,7 @@ def test_zstd_decompression_bomb_rejected(self) -> None: # the bomb's declared decompressed size exceeds the cap. with ( FetchConfig(parallel_threshold_bytes=10_000_000, max_fetch_bytes=512 * 1024) as config, - aioresponses() as mock, + mock_aiohttp() as mock, ): mock.head( url, diff --git a/tests/test_http.py b/tests/test_http.py index 27cb1e6..e80a220 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -21,10 +21,10 @@ import pyarrow as pa import pyarrow.compute as pc import pytest -from aioresponses import CallbackResult -from aioresponses import aioresponses as aioresponses_ctx from pyarrow import ipc +from tests._aiomock import CallbackResult, aiointercept +from tests._aiomock import mock_aiohttp as aiointercept_ctx from vgi_rpc.external import ( ExternalLocationConfig, UploadUrl, @@ -750,7 +750,7 @@ def _make_client(self, config: ExternalLocationConfig) -> _SyncTestClient: server = RpcServer(_ExternalService, _ExternalServiceImpl(), external_location=config) return make_sync_client(server, token_key=b"test-key") - def _mock_aio_dynamic(self, storage: MockStorage, mock: aioresponses_ctx) -> None: + def _mock_aio_dynamic(self, storage: MockStorage, mock: aiointercept) -> None: """Register pattern-based HEAD + GET callbacks that serve from MockStorage dynamically.""" pattern = re.compile(r"^https://mock\.storage/.*$") @@ -778,7 +778,7 @@ def test_unary_large_externalized(self) -> None: config = self._make_config(storage, threshold=10) client = self._make_client(config) - with aioresponses_ctx() as mock: + with aiointercept_ctx() as mock: self._mock_aio_dynamic(storage, mock) with http_connect(_ExternalService, client=client, external_location=config) as proxy: result = proxy.echo_large(data="x" * 200) @@ -808,7 +808,7 @@ def test_server_stream_large_externalized(self) -> None: received_logs: list[Message] = [] - with aioresponses_ctx() as mock: + with aiointercept_ctx() as mock: self._mock_aio_dynamic(storage, mock) with http_connect( _ExternalService, client=client, on_log=received_logs.append, external_location=config @@ -830,7 +830,7 @@ def test_server_stream_with_logs(self) -> None: received_logs: list[Message] = [] - with aioresponses_ctx() as mock: + with aiointercept_ctx() as mock: self._mock_aio_dynamic(storage, mock) with http_connect( _ExternalService, client=client, on_log=received_logs.append, external_location=config @@ -849,7 +849,7 @@ def test_bidi_large_output_externalized(self) -> None: config = self._make_config(storage, threshold=100) client = self._make_client(config) - with aioresponses_ctx() as mock: + with aiointercept_ctx() as mock: self._mock_aio_dynamic(storage, mock) with http_connect(_ExternalService, client=client, external_location=config) as proxy: bidi = proxy.bidi_large(factor=2.0) @@ -871,7 +871,7 @@ def test_bidi_large_input_externalized(self) -> None: config = self._make_config(storage, threshold=100) client = self._make_client(config) - with aioresponses_ctx() as mock: + with aiointercept_ctx() as mock: self._mock_aio_dynamic(storage, mock) with http_connect(_ExternalService, client=client, external_location=config) as proxy: bidi = proxy.bidi_large(factor=3.0) @@ -919,7 +919,7 @@ def _make_client(self, config: ExternalLocationConfig) -> _SyncTestClient: server = RpcServer(_ExternalService, _ExternalServiceImpl(), external_location=config) return make_sync_client(server, token_key=b"test-key") - def _mock_aio_dynamic(self, storage: MockStorage, mock: aioresponses_ctx) -> None: + def _mock_aio_dynamic(self, storage: MockStorage, mock: aiointercept) -> None: """Register pattern-based HEAD + GET callbacks.""" pattern = re.compile(r"^https://mock\.storage/.*$") @@ -949,7 +949,7 @@ def test_sha256_present_on_http_externalize(self) -> None: config = self._make_config(storage, threshold=10) client = self._make_client(config) - with aioresponses_ctx() as mock: + with aiointercept_ctx() as mock: self._mock_aio_dynamic(storage, mock) with http_connect(_ExternalService, client=client, external_location=config) as proxy: result = proxy.echo_large(data="x" * 200) @@ -969,7 +969,7 @@ def test_sha256_verified_on_http_roundtrip(self) -> None: config = self._make_config(storage, threshold=10) client = self._make_client(config) - with aioresponses_ctx() as mock: + with aiointercept_ctx() as mock: self._mock_aio_dynamic(storage, mock) with http_connect(_ExternalService, client=client, external_location=config) as proxy: result = proxy.echo_large(data="hello world " * 50) @@ -999,7 +999,7 @@ def test_sha256_mismatch_over_http(self) -> None: pointer, cm = make_external_location_batch(schema, url, sha256="0" * 64) with ( - aioresponses_ctx() as mock, + aiointercept_ctx() as mock, pytest.raises(RuntimeError, match="SHA-256 checksum mismatch"), ): self._mock_aio_dynamic(storage, mock) @@ -1013,7 +1013,7 @@ def test_sha256_http_stream_externalized(self) -> None: received_logs: list[Message] = [] - with aioresponses_ctx() as mock: + with aiointercept_ctx() as mock: self._mock_aio_dynamic(storage, mock) with http_connect( _ExternalService, client=client, on_log=received_logs.append, external_location=config diff --git a/tests/test_otel.py b/tests/test_otel.py index d03183b..c2d78dc 100644 --- a/tests/test_otel.py +++ b/tests/test_otel.py @@ -14,7 +14,6 @@ import pyarrow as pa import pytest -from aioresponses import aioresponses as aioresponses_ctx from opentelemetry.metrics import MeterProvider from opentelemetry.sdk.metrics import MeterProvider as SdkMeterProvider from opentelemetry.sdk.metrics.export import InMemoryMetricReader @@ -25,6 +24,8 @@ from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator from pyarrow import ipc +from tests._aiomock import aiointercept +from tests._aiomock import mock_aiohttp as aiointercept_ctx from vgi_rpc.external import ( ExternalLocationConfig, ExternalStorage, @@ -1070,9 +1071,9 @@ def _serialize_ipc( @contextmanager -def _mock_aio(storage: _MockStorage) -> Iterator[aioresponses_ctx]: - """Register all MockStorage URLs in aioresponses.""" - with aioresponses_ctx() as mock: +def _mock_aio(storage: _MockStorage) -> Iterator[aiointercept]: + """Register all MockStorage URLs in aiointercept.""" + with aiointercept_ctx() as mock: for url, body in storage.data.items(): headers = {"Content-Length": str(len(body))} mock.head(url, headers=headers) @@ -1202,9 +1203,9 @@ def test_fetch_span_error( fetch_config=FetchConfig(parallel_threshold_bytes=999999), ) - with aioresponses_ctx() as mock: - mock.head(url, exception=OSError("connection refused")) - mock.get(url, exception=OSError("connection refused")) + with aiointercept_ctx() as mock: + mock.head(url, exception=True) + mock.get(url, exception=True) with pytest.raises(RuntimeError, match="Failed to resolve"): resolve_external_location(pointer, cm, config) diff --git a/uv.lock b/uv.lock index 50cefd7..7256000 100644 --- a/uv.lock +++ b/uv.lock @@ -18,7 +18,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.13.5" +version = "3.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -29,72 +29,86 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, - { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, - { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, - { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, - { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, - { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, - { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, - { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, - { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, - { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, - { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, - { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, - { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, - { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, - { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, - { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, - { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, - { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, - { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, - { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, - { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, - { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, - { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, - { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, - { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, - { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, - { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, - { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, - { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, - { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, - { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, - { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, - { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, - { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, - { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, - { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, - { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, - { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, - { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, - { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, - { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, - { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, - { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, - { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, - { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, - { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, - { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, - { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, -] - -[[package]] -name = "aioresponses" -version = "0.7.8" +sdist = { url = "https://files.pythonhosted.org/packages/82/78/8ea7308cac6934de8c74a14f3d5f65d1c89287426688be79538d0e5c013d/aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035", size = 7955794, upload-time = "2026-06-07T21:09:35.529Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/97/bd137012dd97e1649162b099135a80e1fd59aaa807b2430fc448d1029aff/aiohttp-3.14.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178", size = 506882, upload-time = "2026-06-07T21:07:15.501Z" }, + { url = "https://files.pythonhosted.org/packages/ef/79/e5cc690e9d922a66887ceeaca53a8ffd5a7b0be3816142b7abc433742d89/aiohttp-3.14.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf", size = 515270, upload-time = "2026-06-07T21:07:17.53Z" }, + { url = "https://files.pythonhosted.org/packages/fe/22/a73ccbf9dbd6e26dda0b24d5fd5db7da92ee3383a79f47677ffb834c5c5b/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd", size = 485841, upload-time = "2026-06-07T21:07:19.555Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b9/57ed8eaf596321c2ad747bd480fb1700dbd7177c60dfc9e4c187f629662e/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe", size = 492088, upload-time = "2026-06-07T21:07:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/78/c0/5ebe5270a7c140d7c6f79dcb018640225f14d406c149e4eec04a7d82fe71/aiohttp-3.14.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4", size = 501564, upload-time = "2026-06-07T21:07:23.388Z" }, + { url = "https://files.pythonhosted.org/packages/75/7f/8cdaa24fc7983865e0915153b96a9ac5bcdd3548d64c5a27d17cecccad2d/aiohttp-3.14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876", size = 751998, upload-time = "2026-06-07T21:07:25.046Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f4/c4227aacfacc5cb0cc2d119b65301d177912a6842cd64e120c47af76064f/aiohttp-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da", size = 510918, upload-time = "2026-06-07T21:07:27.28Z" }, + { url = "https://files.pythonhosted.org/packages/ab/01/a2d5f96cd4e74424864d30bc0a7e44d0a12dacdcfa91b5b2d1bd3dca6bf3/aiohttp-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6", size = 508657, upload-time = "2026-06-07T21:07:29.252Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ed/3c0fb5c500fdd8e7ebc10d1889c04384fffa1a9163eac1356088ca9da1b1/aiohttp-3.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96", size = 1757907, upload-time = "2026-06-07T21:07:31.03Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ab/d4c924d9bd5be3050c226612413ce68cb54c70d2c31b661bfc8d9a5b6a70/aiohttp-3.14.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f", size = 1737565, upload-time = "2026-06-07T21:07:33.031Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/37326821ff779084020cdc33224d20b19f42f4183a500ff92022a739eda7/aiohttp-3.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296", size = 1799018, upload-time = "2026-06-07T21:07:35.003Z" }, + { url = "https://files.pythonhosted.org/packages/b3/4f/6e947ba73e4ce09070761c05ed3a8ceb7c21f5e46798671d8b2aac0e4626/aiohttp-3.14.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa", size = 1894416, upload-time = "2026-06-07T21:07:36.956Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6e/dbf1d0625dc711fb2851f4f3c3055c39ed58bae92082d8c627dbe6013736/aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451", size = 1783881, upload-time = "2026-06-07T21:07:39.063Z" }, + { url = "https://files.pythonhosted.org/packages/44/c2/5e25098a67268ed369483ae7d1a58bd0a13d03aab860d2a0e4a6eb25b046/aiohttp-3.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c", size = 1587572, upload-time = "2026-06-07T21:07:41.058Z" }, + { url = "https://files.pythonhosted.org/packages/2a/bd/cf9cee17e140f942a3de73e658a543aa8fbf35a5fc67a9d2538d52d77f0b/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca", size = 1722137, upload-time = "2026-06-07T21:07:43.014Z" }, + { url = "https://files.pythonhosted.org/packages/89/6d/5684f8c59045c96f81a18cefbc1fbbd79d25b88f1c622f2a5c5c08fcb632/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09", size = 1755953, upload-time = "2026-06-07T21:07:45.933Z" }, + { url = "https://files.pythonhosted.org/packages/a8/40/35caf3170f8359760740a7d9aa0fff2e344bef98e1d1186f5a0f6dec17e6/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397", size = 1766479, upload-time = "2026-06-07T21:07:48.047Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a1/b0c61e7a137f0d81de49a82023a6df73c3c16d6fefb0f8e4a93d21639002/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080", size = 1580077, upload-time = "2026-06-07T21:07:50.069Z" }, + { url = "https://files.pythonhosted.org/packages/0b/41/194ea4623693009fcefebef7aef63c141754f153e9cd0d39d3b9e36c175c/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345", size = 1791688, upload-time = "2026-06-07T21:07:52.106Z" }, + { url = "https://files.pythonhosted.org/packages/ba/45/4de841f005cfe1fd63e2a2fe011262c515e2a62aa6994b15947e7d717ac9/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588", size = 1761094, upload-time = "2026-06-07T21:07:54.113Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ae/dbce10533d3896d544d5053939ed75b7dc31a1b0973d959b1b5ae21028d6/aiohttp-3.14.1-cp313-cp313-win32.whl", hash = "sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780", size = 452662, upload-time = "2026-06-07T21:07:56.06Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/0bf1a19362c32f06229da5e7ddfcec91f93474d6307f7a2d3135e9c674dc/aiohttp-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a", size = 479748, upload-time = "2026-06-07T21:07:58.319Z" }, + { url = "https://files.pythonhosted.org/packages/22/0a/62e7232dc9484fbec112ceb32efb6a624cc7994ec6e2b019286f17c4e8f2/aiohttp-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8", size = 447723, upload-time = "2026-06-07T21:08:00.154Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a1/5fafa04e1ca91ddb47608699d60649c1c6db3cf41c99e78fc4056f9513db/aiohttp-3.14.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15", size = 508531, upload-time = "2026-06-07T21:08:02.093Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2e/bfa02f699d87ffc86d5959270b28f1cb410add3ccaced8ed2e0b8a5238fc/aiohttp-3.14.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8", size = 514718, upload-time = "2026-06-07T21:08:04.476Z" }, + { url = "https://files.pythonhosted.org/packages/85/a5/9594ad6289eebbc97d167c44213d557807f90e59115caad24de21ad2c3b1/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3", size = 487918, upload-time = "2026-06-07T21:08:06.377Z" }, + { url = "https://files.pythonhosted.org/packages/b4/61/16a32c36c3c49edec122a3dc811f2057df2f94d3b14aa107c8017d981618/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba", size = 494014, upload-time = "2026-06-07T21:08:08.263Z" }, + { url = "https://files.pythonhosted.org/packages/9b/89/3ebcf96ed99c05bec9c434aaac6963fd3cbab4a786ae739908a144d9ce44/aiohttp-3.14.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397", size = 502398, upload-time = "2026-06-07T21:08:10.244Z" }, + { url = "https://files.pythonhosted.org/packages/fd/3d/b74870a0c2d40c355928cd5b96c7a11fa821b8a40fc41365e64479b151fb/aiohttp-3.14.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448", size = 758018, upload-time = "2026-06-07T21:08:12.447Z" }, + { url = "https://files.pythonhosted.org/packages/d3/66/f42f5c984d99e49c6cff5f26f590750f2e2f7ef1fcfb99966ab5be1b632e/aiohttp-3.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004", size = 512462, upload-time = "2026-06-07T21:08:14.624Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a7/248e1aebe0c7810b0271e021a0f2a5eb6e78a051885b3c9df49f42a5802d/aiohttp-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983", size = 512824, upload-time = "2026-06-07T21:08:16.572Z" }, + { url = "https://files.pythonhosted.org/packages/26/97/2aa0e5ba0727dc3bd5aaebb7ccbc510f7dfb7fb961ec87497cd496635ab1/aiohttp-3.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe", size = 1749898, upload-time = "2026-06-07T21:08:18.635Z" }, + { url = "https://files.pythonhosted.org/packages/00/8d/e97f6c96c891d457c8479d92a514ba194d0412f981d72c70341ee18488ed/aiohttp-3.14.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333", size = 1710114, upload-time = "2026-06-07T21:08:20.892Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e6/aa8d7e863048c8fceb5cd6ce74017311cec3ead07847387e12265fb4444e/aiohttp-3.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0", size = 1802541, upload-time = "2026-06-07T21:08:23.044Z" }, + { url = "https://files.pythonhosted.org/packages/83/a8/72193137de57fda4ebfae4563182d082c8856e3b6e9871d0b46f028fb369/aiohttp-3.14.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a", size = 1875776, upload-time = "2026-06-07T21:08:25.288Z" }, + { url = "https://files.pythonhosted.org/packages/a0/18/938441025db6769a3464596b2410af3afde0b21eb2f204c6f766f68af4bd/aiohttp-3.14.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602", size = 1760329, upload-time = "2026-06-07T21:08:27.363Z" }, + { url = "https://files.pythonhosted.org/packages/60/29/bf2496b4065e76e09fe48015aaffe5ce161d8f089b06ac6982070f653076/aiohttp-3.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca", size = 1587293, upload-time = "2026-06-07T21:08:29.805Z" }, + { url = "https://files.pythonhosted.org/packages/49/a2/2136674d52123b1354bd05dd5753c318db47dc0c927cc70b27bab3755456/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35", size = 1714756, upload-time = "2026-06-07T21:08:32.094Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b9/e5fd2e6f915503081c0f9b1e8540947037929c70c191da2e4d54b31a21a1/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844", size = 1721052, upload-time = "2026-06-07T21:08:34.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/5a/2833e324a2263e104e31e2e91bc5bbee81bc499afd32203faee048a883f0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496", size = 1766888, upload-time = "2026-06-07T21:08:36.95Z" }, + { url = "https://files.pythonhosted.org/packages/57/fa/dea6511870913162f3b2e8c42a7614eb203a4540b8c2da43e0bfb0548f3c/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5", size = 1581679, upload-time = "2026-06-07T21:08:39.292Z" }, + { url = "https://files.pythonhosted.org/packages/14/bd/3cf0d55e71784b33534e9710a67d382d900598b4787fbce6cc7317f8c42a/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95", size = 1782021, upload-time = "2026-06-07T21:08:41.407Z" }, + { url = "https://files.pythonhosted.org/packages/c1/af/14bb5843eccbe234f4dfb78ab73e549d99727247e62ae5d62cbd22eaf5b0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444", size = 1742574, upload-time = "2026-06-07T21:08:43.795Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1e/fbeb7af9210a67ac0f9c9bec0f8f4568497924e33137a3d5b48e1cf85f3f/aiohttp-3.14.1-cp314-cp314-win32.whl", hash = "sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0", size = 457773, upload-time = "2026-06-07T21:08:46.168Z" }, + { url = "https://files.pythonhosted.org/packages/f0/2b/13e8d741a9ec5db7d900c060554cf8352ab85e44e2a4469ebb9d377bda17/aiohttp-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719", size = 485001, upload-time = "2026-06-07T21:08:48.401Z" }, + { url = "https://files.pythonhosted.org/packages/df/30/491acfa2c4d6c3ff59c49a14fc1b50be3241e25bbb0c84c09e2da4d11395/aiohttp-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec", size = 453809, upload-time = "2026-06-07T21:08:50.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/e3/19dbe1a1f4cc6230eb9e314de7fe68053b0992f9302b27d12141a0b5db53/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2", size = 793320, upload-time = "2026-06-07T21:08:52.775Z" }, + { url = "https://files.pythonhosted.org/packages/7f/20/1b7182219ba1b108430d6e4dc53d25ae02dcfcf5a045b33af4e8c5167527/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340", size = 529077, upload-time = "2026-06-07T21:08:55Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c8/14ce60ec31a2e5f5274bb17d383a6f7a3aabca31ac04eee05585bbadab16/aiohttp-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d", size = 532476, upload-time = "2026-06-07T21:08:57.176Z" }, + { url = "https://files.pythonhosted.org/packages/7e/02/9ac85e081e53da2e061b02fa7758fe0a12d17b8ce2d1f5e6c7cb76730328/aiohttp-3.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94", size = 1922347, upload-time = "2026-06-07T21:08:59.563Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3e/d3ba07a0ab38b5389e10bec4362d21e10a4f667cba2d79ba30837b3a5059/aiohttp-3.14.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc", size = 1786465, upload-time = "2026-06-07T21:09:01.909Z" }, + { url = "https://files.pythonhosted.org/packages/0b/cb/e2ee978a00cfb2df829704a69528b18154eba5939f45bc1efa8f33aee4c5/aiohttp-3.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251", size = 1909423, upload-time = "2026-06-07T21:09:04.357Z" }, + { url = "https://files.pythonhosted.org/packages/73/5d/1430334858b1022b58ae50399a918f0bd6fe8fa7fa183598d657ff61e040/aiohttp-3.14.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1", size = 2001906, upload-time = "2026-06-07T21:09:06.722Z" }, + { url = "https://files.pythonhosted.org/packages/66/4e/560c7472d3d198a23aa5c8b19a5115bf6a9b77b7d3e4bb363da320430ad2/aiohttp-3.14.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3", size = 1877095, upload-time = "2026-06-07T21:09:09.011Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f1/4745806578d447db4a784a8591e2dae3afdfc2bcb96f8f81271b13df6543/aiohttp-3.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c", size = 1676222, upload-time = "2026-06-07T21:09:11.461Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c9/48255813cca749a229ef0ab476004ec623728ad79a9c0840616f6c076325/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203", size = 1842922, upload-time = "2026-06-07T21:09:14.118Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c0/bbd054e2bee909f529523a5af3891052606af5143c09f5f183ec3b234676/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1", size = 1825035, upload-time = "2026-06-07T21:09:16.447Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ae/90395d4376deceb74e09ec26b6adf7d2015a6f8802d6d84446af860fef04/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665", size = 1849512, upload-time = "2026-06-07T21:09:18.742Z" }, + { url = "https://files.pythonhosted.org/packages/93/bd/fb25f3049957553d4ce0ba6ae480aa2f592a6985497fca590837d16c1be0/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1", size = 1668571, upload-time = "2026-06-07T21:09:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/3f/22/7f73303d64dd567ff3addca90b556690ed1233a47b8f55d242fb90af3681/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d", size = 1881159, upload-time = "2026-06-07T21:09:23.813Z" }, + { url = "https://files.pythonhosted.org/packages/44/be/0474c5a8b5640e1e4aa1923430a91f4151be82e511373fe764189b89aef5/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c", size = 1841409, upload-time = "2026-06-07T21:09:26.207Z" }, + { url = "https://files.pythonhosted.org/packages/7b/3c/bb4a7cba26956cb3da4553cc2056cf67be5b5ff6e6d8fa4fbdff73bfb7ae/aiohttp-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365", size = 494166, upload-time = "2026-06-07T21:09:28.505Z" }, + { url = "https://files.pythonhosted.org/packages/8a/84/ec80c2c1f66a952555a9f86df6b33af65108a6febfa0471b69013a12f807/aiohttp-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9", size = 530255, upload-time = "2026-06-07T21:09:30.843Z" }, + { url = "https://files.pythonhosted.org/packages/2a/71/6e22be134a4061ada85a92951b842f2657f17d926b727f3f94c56ae963d6/aiohttp-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6", size = 469640, upload-time = "2026-06-07T21:09:33.028Z" }, +] + +[[package]] +name = "aiointercept" +version = "0.1.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, - { name = "packaging" }, + { name = "multidict" }, + { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/03/532bbc645bdebcf3b6af3b25d46655259d66ce69abba7720b71ebfabbade/aioresponses-0.7.8.tar.gz", hash = "sha256:b861cdfe5dc58f3b8afac7b0a6973d5d7b2cb608dd0f6253d16b8ee8eaf6df11", size = 40253, upload-time = "2025-01-19T18:14:03.222Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/de/637bd02b6d6ab6ff3b11e3726b0bbbf7878e7acc649b509ad91026a1a838/aiointercept-0.1.7.tar.gz", hash = "sha256:b9681ec590457e062159c8bdb1d77f17b9a80521a95c779010d55bd8c5d585cc", size = 156459, upload-time = "2026-06-12T09:31:38.518Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b7/584157e43c98aa89810bc2f7099e7e01c728ecf905a66cf705106009228f/aioresponses-0.7.8-py2.py3-none-any.whl", hash = "sha256:b73bd4400d978855e55004b23a3a84cb0f018183bcf066a85ad392800b5b9a94", size = 12518, upload-time = "2025-01-19T18:13:59.633Z" }, + { url = "https://files.pythonhosted.org/packages/a7/be/34c2ae1347845e87fbb613ce4f9ea0492c680feace356cd7e2a24c33e1bc/aiointercept-0.1.7-py3-none-any.whl", hash = "sha256:953d3c2d27e93e710d655ea47d191376e2966ca432a99fcfe2c780f9965e1212", size = 18010, upload-time = "2026-06-12T09:31:37.286Z" }, ] [[package]] @@ -2581,7 +2595,7 @@ sentry = [ [package.dev-dependencies] dev = [ - { name = "aioresponses" }, + { name = "aiointercept" }, { name = "boto3" }, { name = "cryptography" }, { name = "falcon" }, @@ -2625,7 +2639,7 @@ docs = [ [package.metadata] requires-dist = [ - { name = "aiohttp", marker = "extra == 'external'", specifier = ">=3.9,<3.14" }, + { name = "aiohttp", marker = "extra == 'external'", specifier = ">=3.14.1" }, { name = "boto3", marker = "extra == 's3'", specifier = ">=1.28" }, { name = "cryptography", marker = "extra == 'mtls'", specifier = ">=41.0" }, { name = "docstring-parser", specifier = ">=0.16" }, @@ -2653,7 +2667,7 @@ provides-extras = ["http", "s3", "gcs", "cli", "external", "otel", "oauth", "mtl [package.metadata.requires-dev] dev = [ - { name = "aioresponses", specifier = ">=0.7" }, + { name = "aiointercept", specifier = ">=0.1.7" }, { name = "boto3", specifier = ">=1.28" }, { name = "cryptography", specifier = ">=41.0" }, { name = "falcon", specifier = ">=3.0" },