Sync contents

This commit is contained in:
Andrii Grynenko 2026-03-09 00:49:41 +00:00
parent 77cc8a0810
commit 35a77e79fe
15 changed files with 2186 additions and 152 deletions

View file

@ -1,4 +1,4 @@
{
"last_synced_sha": "7a5a376dc40cd21c3c744270af661b021e9fa9c6",
"last_sync_time": "2026-02-27T05:49:39.458251"
"last_synced_sha": "b31ac99f31c26082a4076c3c79719034b2e0cab2",
"last_sync_time": "2026-03-09T00:49:41.558594"
}

View file

@ -13,6 +13,8 @@ The ServiceClient is the main entry point for the Tinker API. It provides method
- Generate RestClient instances for REST API operations like listing weights
Args:
user_metadata: Optional metadata attached to the created session.
project_id: Optional project ID to attach to the created session.
**kwargs: advanced options passed to the underlying HTTP client,
including API keys, headers, and connection settings.

View file

@ -1,6 +1,6 @@
[project]
name = "tinker"
version = "0.14.0"
version = "0.15.0"
description = "The official Python SDK for the tinker API"
readme = "README.md"
license = "Apache-2.0"

View file

@ -89,7 +89,7 @@ class AsyncTinker(AsyncAPIClient):
if base_url is None:
base_url = os.environ.get("TINKER_BASE_URL")
if base_url is None:
if base_url is None or base_url == "":
base_url = "https://tinker.thinkingmachines.dev/services/tinker-prod"
super().__init__(

View file

@ -14,6 +14,10 @@ __all__ = [
"RateLimitError",
"InternalServerError",
"RequestFailedError",
"SidecarError",
"SidecarStartupError",
"SidecarDiedError",
"SidecarIPCError",
]
if TYPE_CHECKING:
@ -166,6 +170,22 @@ class InternalServerError(APIStatusError):
pass
class SidecarError(TinkerError):
"""Base exception for subprocess sidecar errors."""
class SidecarStartupError(SidecarError):
"""Raised when the sidecar subprocess fails to start or times out."""
class SidecarDiedError(SidecarError):
"""Raised when the sidecar subprocess exits unexpectedly while requests are pending."""
class SidecarIPCError(SidecarError):
"""Raised when communication with the sidecar subprocess fails."""
class RequestFailedError(TinkerError):
"""Raised when an asynchronous request completes in a failed state."""

View file

@ -85,6 +85,8 @@ class _APIFuture(APIFuture[T]): # pyright: ignore[reportUnusedClass]
start_time = time.time()
iteration = -1
connection_error_retries = 0
bad_request_retries = 0
MAX_BAD_REQUEST_RETRIES = 3
allow_metadata_only = True
async with contextlib.AsyncExitStack() as stack:
@ -135,9 +137,7 @@ class _APIFuture(APIFuture[T]): # pyright: ignore[reportUnusedClass]
user_error = is_user_error(e)
if telemetry := self.get_telemetry():
current_time = time.time()
telemetry.log(
"APIFuture.result_async.api_status_error",
event_data={
event_data: dict[str, object] = {
"request_id": self.request_id,
"request_type": self.request_type,
"status_code": e.status_code,
@ -146,12 +146,20 @@ class _APIFuture(APIFuture[T]): # pyright: ignore[reportUnusedClass]
"is_user_error": user_error,
"iteration": iteration,
"elapsed_time": current_time - start_time,
},
}
if not should_retry:
event_data["response_headers"] = dict(e.response.headers)
event_data["response_body"] = e.body
event_data["bad_request_retries"] = bad_request_retries
telemetry.log(
"APIFuture.result_async.api_status_error",
event_data=event_data,
severity="WARNING" if should_retry or user_error else "ERROR",
)
# Retry 408s until we time out
if e.status_code == 408:
bad_request_retries = 0
if self._queue_state_observer is not None:
with contextlib.suppress(Exception):
response = e.response.json()
@ -175,6 +183,11 @@ class _APIFuture(APIFuture[T]): # pyright: ignore[reportUnusedClass]
) from e
if e.status_code in range(500, 600):
continue
# Retry 400s a few times — a bare 400 with no body may come from
# a load balancer indicating a bad connection rather than the API.
if e.status_code == 400 and bad_request_retries < MAX_BAD_REQUEST_RETRIES:
bad_request_retries += 1
continue
raise ValueError(
f"Error retrieving result: {e} with status code {e.status_code=} for {self.request_id=} and expected type {self.model_cls=}"
) from e
@ -289,7 +302,11 @@ class _APIFuture(APIFuture[T]): # pyright: ignore[reportUnusedClass]
return self._future.result(timeout)
async def result_async(self, timeout: float | None = None) -> T:
try:
return await asyncio.wait_for(self._future, timeout)
except (asyncio.CancelledError, asyncio.TimeoutError):
self._future.future().cancel()
raise
def get_telemetry(self) -> Telemetry | None:
return self.holder.get_telemetry()

View file

@ -90,6 +90,19 @@ class InternalClientHolderThreadSingleton:
assert self._loop is not None, "Loop must not be None"
self._loop.run_forever()
def _set_loop(self, loop: asyncio.AbstractEventLoop) -> None:
"""Inject an external event loop (e.g. the sidecar subprocess loop).
Must be called before any InternalClientHolder is created.
Prevents _ensure_started from spawning a background thread the
caller's loop is used directly.
"""
with self._lifecycle_lock:
if self._started:
raise RuntimeError("Cannot set_loop after singleton has started")
self._loop = loop
self._started = True # prevent _ensure_started from creating a thread
def get_loop(self) -> asyncio.AbstractEventLoop:
self._ensure_started()
assert self._loop is not None, "Loop must not be None"
@ -151,6 +164,7 @@ class InternalClientHolder(AsyncTinkerProvider, TelemetryProvider):
def __init__(
self,
user_metadata: dict[str, str] | None = None,
project_id: str | None = None,
*,
session_id: str | None = None,
**kwargs: Any,
@ -171,14 +185,29 @@ class InternalClientHolder(AsyncTinkerProvider, TelemetryProvider):
self._training_client_counter: int | None = None
self._sampling_client_counter: int | None = None
else:
# Normal mode: create new session
# Normal mode: create new session.
# This blocks on .result() — must NOT be called from the event
# loop thread (e.g. inside the sidecar subprocess). Shadow
# holders (session_id is not None) skip this path.
if self._loop.is_running() and _current_loop() is self._loop:
raise RuntimeError(
"Cannot create a new session from the event loop thread. "
"Use session_id= to create a shadow holder instead."
)
self._session_id = self.run_coroutine_threadsafe(
self._create_session(user_metadata)
self._create_session(user_metadata=user_metadata, project_id=project_id)
).result()
self._training_client_counter = 0
self._sampling_client_counter = 0
self._session_heartbeat_task: asyncio.Task[None] = self.run_coroutine_threadsafe(
if self._loop.is_running() and _current_loop() is self._loop:
# Already on the event loop thread — .result() would deadlock.
# Create the heartbeat task directly instead of via run_coroutine_threadsafe.
self._session_heartbeat_task: asyncio.Task[None] = asyncio.create_task(
self._session_heartbeat(self._session_id)
)
else:
self._session_heartbeat_task = self.run_coroutine_threadsafe(
self._start_heartbeat()
).result()
self._telemetry: Telemetry | None = init_telemetry(self, session_id=self._session_id)
@ -274,14 +303,21 @@ class InternalClientHolder(AsyncTinkerProvider, TelemetryProvider):
"""Start the session heartbeat task."""
return asyncio.create_task(self._session_heartbeat(self._session_id))
async def _create_session(self, user_metadata: dict[str, str] | None = None) -> str:
async def _create_session(
self,
user_metadata: dict[str, str] | None = None,
project_id: str | None = None,
) -> str:
if (tags_str := os.environ.get("TINKER_TAGS")) is not None:
tags: set[str] = set(tags_str.split(","))
else:
tags = set()
with self.aclient(ClientConnectionPoolType.SESSION) as client:
request = types.CreateSessionRequest(
tags=list(tags), user_metadata=user_metadata or {}, sdk_version=tinker_sdk_version
tags=list(tags),
user_metadata=user_metadata or {},
sdk_version=tinker_sdk_version,
project_id=project_id,
)
result = await client.service.create_session(request=request)
if result.info_message:

View file

@ -5,7 +5,7 @@ from __future__ import annotations
import asyncio
import logging
from concurrent.futures import Future as ConcurrentFuture
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Literal
from tinker import types
from tinker._types import NoneType
@ -60,7 +60,9 @@ class RestClient(TelemetryProvider):
self.holder = holder
def _get_training_run_submit(
self, training_run_id: types.ModelID
self,
training_run_id: types.ModelID,
access_scope: Literal["owned", "accessible"] = "owned",
) -> AwaitableConcurrentFuture[types.TrainingRun]:
"""Internal method to submit get model request."""
@ -69,6 +71,7 @@ class RestClient(TelemetryProvider):
with self.holder.aclient(ClientConnectionPoolType.TRAIN) as client:
return await client.get(
f"/api/v1/training_runs/{training_run_id}",
options={"params": {"access_scope": access_scope}},
cast_to=types.TrainingRun,
)
@ -79,7 +82,9 @@ class RestClient(TelemetryProvider):
@sync_only
@capture_exceptions(fatal=True)
def get_training_run(
self, training_run_id: types.ModelID
self,
training_run_id: types.ModelID,
access_scope: Literal["owned", "accessible"] = "owned",
) -> ConcurrentFuture[types.TrainingRun]:
"""Get training run info.
@ -96,17 +101,19 @@ class RestClient(TelemetryProvider):
print(f"Training Run ID: {response.training_run_id}, Base: {response.base_model}")
```
"""
return self._get_training_run_submit(training_run_id).future()
return self._get_training_run_submit(training_run_id, access_scope=access_scope).future()
@capture_exceptions(fatal=True)
async def get_training_run_async(self, training_run_id: types.ModelID) -> types.TrainingRun:
async def get_training_run_async(
self, training_run_id: types.ModelID, access_scope: Literal["owned", "accessible"] = "owned"
) -> types.TrainingRun:
"""Async version of get_training_run."""
return await self._get_training_run_submit(training_run_id)
return await self._get_training_run_submit(training_run_id, access_scope=access_scope)
@sync_only
@capture_exceptions(fatal=True)
def get_training_run_by_tinker_path(
self, tinker_path: str
self, tinker_path: str, access_scope: Literal["owned", "accessible"] = "owned"
) -> ConcurrentFuture[types.TrainingRun]:
"""Get training run info.
@ -126,15 +133,23 @@ class RestClient(TelemetryProvider):
parsed_checkpoint_tinker_path = types.ParsedCheckpointTinkerPath.from_tinker_path(
tinker_path
)
return self.get_training_run(parsed_checkpoint_tinker_path.training_run_id)
return self.get_training_run(
parsed_checkpoint_tinker_path.training_run_id,
access_scope=access_scope,
)
@capture_exceptions(fatal=True)
async def get_training_run_by_tinker_path_async(self, tinker_path: str) -> types.TrainingRun:
async def get_training_run_by_tinker_path_async(
self, tinker_path: str, access_scope: Literal["owned", "accessible"] = "owned"
) -> types.TrainingRun:
"""Async version of get_training_run_by_tinker_path."""
parsed_checkpoint_tinker_path = types.ParsedCheckpointTinkerPath.from_tinker_path(
tinker_path
)
return await self.get_training_run_async(parsed_checkpoint_tinker_path.training_run_id)
return await self.get_training_run_async(
parsed_checkpoint_tinker_path.training_run_id,
access_scope=access_scope,
)
@capture_exceptions(fatal=True)
def get_weights_info_by_tinker_path(
@ -170,14 +185,21 @@ class RestClient(TelemetryProvider):
return self.holder.run_coroutine_threadsafe(_get_weights_info_async())
def _list_training_runs_submit(
self, limit: int = 20, offset: int = 0
self,
limit: int = 20,
offset: int = 0,
access_scope: Literal["owned", "accessible"] = "owned",
) -> AwaitableConcurrentFuture[types.TrainingRunsResponse]:
"""Internal method to submit list training runs request."""
async def _list_training_runs_async() -> types.TrainingRunsResponse:
async def _send_request() -> types.TrainingRunsResponse:
with self.holder.aclient(ClientConnectionPoolType.TRAIN) as client:
params: dict[str, object] = {"limit": limit, "offset": offset}
params: dict[str, object] = {
"limit": limit,
"offset": offset,
"access_scope": access_scope,
}
return await client.get(
"/api/v1/training_runs",
@ -192,7 +214,10 @@ class RestClient(TelemetryProvider):
@sync_only
@capture_exceptions(fatal=True)
def list_training_runs(
self, limit: int = 20, offset: int = 0
self,
limit: int = 20,
offset: int = 0,
access_scope: Literal["owned", "accessible"] = "owned",
) -> ConcurrentFuture[types.TrainingRunsResponse]:
"""List training runs with pagination support.
@ -213,14 +238,25 @@ class RestClient(TelemetryProvider):
next_page = rest_client.list_training_runs(limit=50, offset=50)
```
"""
return self._list_training_runs_submit(limit, offset).future()
return self._list_training_runs_submit(
limit,
offset,
access_scope=access_scope,
).future()
@capture_exceptions(fatal=True)
async def list_training_runs_async(
self, limit: int = 20, offset: int = 0
self,
limit: int = 20,
offset: int = 0,
access_scope: Literal["owned", "accessible"] = "owned",
) -> types.TrainingRunsResponse:
"""Async version of list_training_runs."""
return await self._list_training_runs_submit(limit, offset)
return await self._list_training_runs_submit(
limit,
offset,
access_scope=access_scope,
)
def _list_checkpoints_submit(
self, training_run_id: types.ModelID
@ -653,7 +689,9 @@ class RestClient(TelemetryProvider):
return await self._list_user_checkpoints_submit(limit, offset)
def _get_session_submit(
self, session_id: str
self,
session_id: str,
access_scope: Literal["owned", "accessible"] = "owned",
) -> AwaitableConcurrentFuture[types.GetSessionResponse]:
"""Internal method to submit get session request."""
@ -662,6 +700,7 @@ class RestClient(TelemetryProvider):
with self.holder.aclient(ClientConnectionPoolType.TRAIN) as client:
return await client.get(
f"/api/v1/sessions/{session_id}",
options={"params": {"access_scope": access_scope}},
cast_to=types.GetSessionResponse,
)
@ -671,7 +710,9 @@ class RestClient(TelemetryProvider):
@sync_only
@capture_exceptions(fatal=True)
def get_session(self, session_id: str) -> ConcurrentFuture[types.GetSessionResponse]:
def get_session(
self, session_id: str, access_scope: Literal["owned", "accessible"] = "owned"
) -> ConcurrentFuture[types.GetSessionResponse]:
"""Get session information including all training runs and samplers.
Args:
@ -688,22 +729,31 @@ class RestClient(TelemetryProvider):
print(f"Samplers: {len(response.sampler_ids)}")
```
"""
return self._get_session_submit(session_id).future()
return self._get_session_submit(session_id, access_scope=access_scope).future()
@capture_exceptions(fatal=True)
async def get_session_async(self, session_id: str) -> types.GetSessionResponse:
async def get_session_async(
self, session_id: str, access_scope: Literal["owned", "accessible"] = "owned"
) -> types.GetSessionResponse:
"""Async version of get_session."""
return await self._get_session_submit(session_id)
return await self._get_session_submit(session_id, access_scope=access_scope)
def _list_sessions_submit(
self, limit: int = 20, offset: int = 0
self,
limit: int = 20,
offset: int = 0,
access_scope: Literal["owned", "accessible"] = "owned",
) -> AwaitableConcurrentFuture[types.ListSessionsResponse]:
"""Internal method to submit list sessions request."""
async def _list_sessions_async() -> types.ListSessionsResponse:
async def _send_request() -> types.ListSessionsResponse:
with self.holder.aclient(ClientConnectionPoolType.TRAIN) as client:
params: dict[str, object] = {"limit": limit, "offset": offset}
params: dict[str, object] = {
"limit": limit,
"offset": offset,
"access_scope": access_scope,
}
return await client.get(
"/api/v1/sessions",
@ -718,7 +768,10 @@ class RestClient(TelemetryProvider):
@sync_only
@capture_exceptions(fatal=True)
def list_sessions(
self, limit: int = 20, offset: int = 0
self,
limit: int = 20,
offset: int = 0,
access_scope: Literal["owned", "accessible"] = "owned",
) -> ConcurrentFuture[types.ListSessionsResponse]:
"""List sessions with pagination support.
@ -738,14 +791,25 @@ class RestClient(TelemetryProvider):
next_page = rest_client.list_sessions(limit=50, offset=50)
```
"""
return self._list_sessions_submit(limit, offset).future()
return self._list_sessions_submit(
limit,
offset,
access_scope=access_scope,
).future()
@capture_exceptions(fatal=True)
async def list_sessions_async(
self, limit: int = 20, offset: int = 0
self,
limit: int = 20,
offset: int = 0,
access_scope: Literal["owned", "accessible"] = "owned",
) -> types.ListSessionsResponse:
"""Async version of list_sessions."""
return await self._list_sessions_submit(limit, offset)
return await self._list_sessions_submit(
limit,
offset,
access_scope=access_scope,
)
@capture_exceptions(fatal=True)
def get_sampler(self, sampler_id: str) -> APIFuture[types.GetSamplerResponse]:

View file

@ -314,16 +314,9 @@ class SamplingClient(TelemetryProvider, QueueStateObserver):
"""Async version of compute_logprobs."""
return await AwaitableConcurrentFuture(self.compute_logprobs(prompt))
@capture_exceptions(fatal=True)
def get_tokenizer(self) -> PreTrainedTokenizer:
"""Get the tokenizer for the current model.
Returns:
- `PreTrainedTokenizer` compatible with the model
"""
async def _get_sampler_async():
async def _send_request():
def _get_sampler_submit(self) -> AwaitableConcurrentFuture[types.GetSamplerResponse]:
async def _get_sampler_async() -> types.GetSamplerResponse:
async def _send_request() -> types.GetSamplerResponse:
with self.holder.aclient(ClientConnectionPoolType.TRAIN) as client:
return await client.get(
f"/api/v1/samplers/{self._sampling_session_id}",
@ -332,9 +325,27 @@ class SamplingClient(TelemetryProvider, QueueStateObserver):
return await self.holder.execute_with_retries(_send_request)
sampler_info = self.holder.run_coroutine_threadsafe(_get_sampler_async()).result()
return self.holder.run_coroutine_threadsafe(_get_sampler_async())
@capture_exceptions(fatal=True)
def get_tokenizer(self) -> PreTrainedTokenizer:
"""Get the tokenizer for the current model.
Returns:
- `PreTrainedTokenizer` compatible with the model
"""
sampler_info = self._get_sampler_submit().result()
return _load_tokenizer_from_model_info(sampler_info.base_model)
@capture_exceptions(fatal=True)
def get_base_model(self) -> str:
"""Get the base model name for the current sampling session."""
return self._get_sampler_submit().result().base_model
async def get_base_model_async(self) -> str:
"""Async version of get_base_model."""
return (await self._get_sampler_submit()).base_model
def get_telemetry(self) -> Telemetry | None:
return self.holder.get_telemetry()

View file

@ -37,6 +37,8 @@ class ServiceClient(TelemetryProvider):
- Generate RestClient instances for REST API operations like listing weights
Args:
user_metadata: Optional metadata attached to the created session.
project_id: Optional project ID to attach to the created session.
**kwargs: advanced options passed to the underlying HTTP client,
including API keys, headers, and connection settings.
@ -56,10 +58,16 @@ class ServiceClient(TelemetryProvider):
```
"""
def __init__(self, user_metadata: dict[str, str] | None = None, **kwargs: Any):
def __init__(
self,
user_metadata: dict[str, str] | None = None,
project_id: str | None = None,
**kwargs: Any,
):
default_headers = _get_default_headers() | kwargs.pop("default_headers", {})
self.holder = InternalClientHolder(
user_metadata=user_metadata,
project_id=project_id,
**kwargs,
default_headers=default_headers,
_strict_response_validation=True,

759
src/tinker/lib/sidecar.py Normal file
View file

@ -0,0 +1,759 @@
"""Subprocess sidecar for GIL-isolated RPC execution.
Runs one or more picklable target objects in a dedicated subprocess and routes
typed RPC calls to them via multiprocessing queues. The subprocess runs a
shared asyncio event loop and self-terminates when the parent process dies.
Usage multiple RPCs on one target::
handle = create_sidecar_handle(Calculator())
@dataclasses.dataclass
class MultiplyRPC(SidecarRPC):
a: int
b: int
async def execute(self, target: Calculator) -> int:
return target.multiply(self.a, self.b)
# Methods that return Futures work too — the sidecar automatically awaits
# them after execute() returns.
@dataclasses.dataclass
class SampleRPC(SidecarRPC):
prompt: str
async def execute(self, target: SamplingClient) -> SampleResponse:
return target.sample(self.prompt)
assert handle.submit_rpc(MultiplyRPC(a=3, b=4)).result() == 12
Multiple targets on the same sidecar::
calc_handle = create_sidecar_handle(Calculator())
db_handle = create_sidecar_handle(DatabaseClient(uri))
# Each handle routes RPCs to its own target
calc_handle.submit_rpc(MultiplyRPC(a=1, b=2))
db_handle.submit_rpc(QueryRPC(sql="SELECT ..."))
"""
from __future__ import annotations
import asyncio
import contextlib
import dataclasses
import inspect
import logging
import multiprocessing
import multiprocessing.connection
import multiprocessing.process
import pickle
import queue
import threading
import time
from concurrent.futures import Future as ConcurrentFuture
from typing import Any
from tinker._exceptions import SidecarDiedError, SidecarIPCError, SidecarStartupError
logger = logging.getLogger(__name__)
# Use "spawn" context to avoid fork issues with background event loop threads.
# Fork would duplicate threads in a broken state; spawn creates a fresh interpreter.
_mp_context = multiprocessing.get_context("spawn")
# Startup protocol messages
_STARTUP_OK = "__startup_ok__"
# Timeouts (seconds)
_STARTUP_TIMEOUT_SECONDS = 30
_COLLECTOR_POLL_INTERVAL_SECONDS = 1.0
# Maximum number of requests to dequeue per event loop tick. This provides
# natural backpressure: when the event loop is busy (many in-flight requests,
# slow networking), ticks slow down and the worker pulls less work.
_MAX_REQUESTS_PER_TICK = 16
def _close_queue(q: multiprocessing.Queue[Any] | None) -> None:
"""Close a queue without blocking. cancel_join_thread() prevents hangs."""
if q is None:
return
with contextlib.suppress(Exception):
q.cancel_join_thread()
q.close()
# ---------------------------------------------------------------------------
# Target registry (subprocess-only, single-threaded event loop)
# ---------------------------------------------------------------------------
_targets: dict[int, Any] = {}
_next_target_id: int = 0
# ---------------------------------------------------------------------------
# RPC protocol
# ---------------------------------------------------------------------------
class SidecarRPC:
"""Base class for sidecar RPC requests.
Subclasses must implement ``async execute(target)`` which runs directly on
the subprocess event loop. ``target`` is the object registered via
``create_sidecar_handle()``.
If the return value is a ``ConcurrentFuture`` or awaitable, the sidecar
automatically awaits it before sending the result back.
RPC objects must be picklable since they are sent through a
``multiprocessing.Queue``.
"""
async def execute(self, target: Any) -> Any:
"""Execute this RPC on the subprocess event loop.
If the return value is a ``ConcurrentFuture`` or awaitable, it is
automatically awaited.
Args:
target: The registered target object for this handle.
"""
raise NotImplementedError
@dataclasses.dataclass
class _RegisterTargetRPC(SidecarRPC):
"""Built-in RPC: register a new target in the subprocess."""
pickled_target: bytes
async def execute(self, target: Any) -> int:
global _next_target_id
unpickled = pickle.loads(self.pickled_target)
target_id = _next_target_id
_next_target_id += 1
_targets[target_id] = unpickled
return target_id
@dataclasses.dataclass
class _UnregisterTargetRPC(SidecarRPC):
"""Built-in RPC: remove a target from the subprocess registry."""
target_id: int
async def execute(self, target: Any) -> None:
_targets.pop(self.target_id, None)
# ---------------------------------------------------------------------------
# SidecarHandle
# ---------------------------------------------------------------------------
class SidecarHandle:
"""Handle for submitting RPCs to a specific target in the sidecar.
Wraps a ``SubprocessSidecar`` and a ``target_id``. Provides the same
``submit_rpc()`` API the handle pairs each RPC with its target ID in the
wire protocol so the subprocess can resolve the target before calling
``execute()``. The RPC object itself is never mutated.
When the handle is garbage-collected or explicitly deleted,
the target is automatically unregistered from the subprocess.
Thread-safe: multiple threads may call ``submit_rpc()`` on the same handle
and may safely reuse the same RPC instance across calls.
Not picklable obtain handles via ``create_sidecar_handle()``
in the process that owns the sidecar.
"""
def __init__(self, sidecar: SubprocessSidecar, target_id: int):
self._sidecar = sidecar
self._target_id = target_id
def submit_rpc(self, rpc: SidecarRPC) -> ConcurrentFuture[Any]:
"""Submit an RPC to the handle's target."""
return self._sidecar._submit_rpc(rpc, target_id=self._target_id)
def __del__(self) -> None:
# Auto-unregister target from subprocess (fire-and-forget).
# Uses bare try/except — not contextlib.suppress — because during
# interpreter shutdown module globals (including contextlib) can be None.
try:
self._sidecar._submit_rpc(_UnregisterTargetRPC(target_id=self._target_id))
except Exception:
pass
def __reduce__(self) -> None: # type: ignore[override]
raise TypeError(
"SidecarHandle cannot be pickled. It holds a reference to a local "
"SubprocessSidecar. Call create_sidecar_handle() in the target process instead."
)
# ---------------------------------------------------------------------------
# Subprocess worker
# ---------------------------------------------------------------------------
# Set to True inside the sidecar subprocess. Checked by callers to avoid
# nesting (daemon processes cannot spawn children).
_inside_sidecar: bool = False
def _subprocess_worker(
request_queue: multiprocessing.Queue[Any],
response_queue: multiprocessing.Queue[Any],
parent_conn: multiprocessing.connection.Connection,
) -> None:
"""Entry point for the sidecar subprocess.
Creates the event loop manually and injects it into the SDK's
``InternalClientHolderThreadSingleton`` **before** any target is unpickled.
This way heartbeats, rate limiting, and RPCs all share one loop with no
extra background thread.
Starts empty (no initial target). Targets are registered via
``_RegisterTargetRPC``. Monitors ``parent_conn`` for parent death
(EOF on pipe -> self-terminate).
"""
global _inside_sidecar
_inside_sidecar = True
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
from tinker.lib.internal_client_holder import _internal_client_holder_thread_singleton
_internal_client_holder_thread_singleton._set_loop(loop)
response_queue.put((_STARTUP_OK, None, None))
try:
loop.run_until_complete(_async_worker_main(request_queue, response_queue, parent_conn))
finally:
loop.close()
async def _async_worker_main(
request_queue: multiprocessing.Queue[Any],
response_queue: multiprocessing.Queue[Any],
parent_conn: multiprocessing.connection.Connection,
) -> None:
"""Async event loop: dequeue RPCs, dispatch as tasks, monitor parent pipe."""
loop = asyncio.get_running_loop()
pending_tasks: set[asyncio.Task[None]] = set()
shutting_down = False
# Monitor parent death via a detached daemon thread. Uses blocking poll(None)
# — when the parent dies, the OS closes the pipe and poll raises EOFError.
# Sets a threading.Event (not asyncio.Event) so it's safe from any thread.
parent_gone = threading.Event()
def _watch_parent() -> None:
try:
parent_conn.poll(None)
except (EOFError, OSError):
pass
parent_gone.set()
watcher = threading.Thread(target=_watch_parent, daemon=True, name="parent-death-watcher")
watcher.start()
while not shutting_down:
# Wait for next request, checking parent liveness between polls.
raw = None
while raw is None and not parent_gone.is_set():
try:
raw = await loop.run_in_executor(
None, lambda: request_queue.get(timeout=_COLLECTOR_POLL_INTERVAL_SECONDS)
)
except queue.Empty:
continue
except Exception:
break
if raw is None or parent_gone.is_set():
break
# Wire format: (request_id, target_id | None, rpc).
# pickle.loads failures are not caught — corrupted payloads crash
# the worker, and the collector fails all pending futures. This is
# the correct response: we serialize payloads ourselves, so
# deserialization failure means the IPC channel is broken.
batch: list[tuple[int, int | None, SidecarRPC]] = [pickle.loads(raw)]
# Non-blocking drain of up to N-1 more requests
for _ in range(_MAX_REQUESTS_PER_TICK - 1):
try:
raw = request_queue.get_nowait()
except Exception:
break
if raw is None:
shutting_down = True
break
batch.append(pickle.loads(raw))
for request_id, target_id, rpc in batch:
task = asyncio.create_task(_handle_request(request_id, target_id, rpc, response_queue))
pending_tasks.add(task)
task.add_done_callback(pending_tasks.discard)
# Yield to let in-flight tasks make progress before pulling more work
await asyncio.sleep(0)
for task in pending_tasks:
task.cancel()
async def _handle_request(
request_id: int,
target_id: int | None,
rpc: SidecarRPC,
response_queue: multiprocessing.Queue[Any],
) -> None:
"""Resolve the target, run execute() on the event loop, bridge the result."""
try:
target = None
if target_id is not None:
target = _targets.get(target_id)
if target is None:
raise RuntimeError(
f"{type(rpc).__name__}: target_id={target_id} is not registered "
"(target was never registered or was already unregistered)"
)
result = await rpc.execute(target)
# If execute() returned a Future or awaitable, await it on the event
# loop.
if isinstance(result, ConcurrentFuture):
result = await asyncio.wrap_future(result)
elif inspect.isawaitable(result):
result = await result
_put_response(response_queue, request_id, result, None)
except Exception as e:
_put_response(response_queue, request_id, None, e)
def _put_response(
response_queue: multiprocessing.Queue[Any],
request_id: int,
result: Any,
exception: BaseException | None,
) -> None:
"""Pre-serialize and enqueue a response. Wraps unpicklable exceptions."""
if exception is not None:
try:
pickle.dumps(exception)
except Exception:
exception = SidecarIPCError(f"{type(exception).__name__}: {exception}")
try:
payload = pickle.dumps((request_id, result, exception))
response_queue.put(payload)
except Exception:
try:
payload = pickle.dumps(
(request_id, None, SidecarIPCError("Failed to serialize response"))
)
response_queue.put(payload)
except Exception:
logger.error(f"Failed to send response for request {request_id} — queue is broken")
# ---------------------------------------------------------------------------
# Response collector thread
# ---------------------------------------------------------------------------
class _ResponseCollector(threading.Thread):
"""Daemon thread that owns all reads from the response queue.
Handles two phases:
1. **Startup handshake** waits for ``_STARTUP_OK`` from the subprocess.
The parent thread blocks on :meth:`wait_ready` until this completes.
2. **Response loop** reads RPC responses and resolves the corresponding
``ConcurrentFuture`` objects.
When the subprocess dies or the queue breaks, all pending futures are
failed with ``SidecarDiedError``.
"""
def __init__(
self,
response_queue: multiprocessing.Queue[Any],
pending_futures: dict[int, ConcurrentFuture[Any]],
pending_lock: threading.Lock,
process: multiprocessing.process.BaseProcess,
):
super().__init__(daemon=True, name="SubprocessSidecar-ResponseCollector")
self._response_queue = response_queue
self._pending_futures = pending_futures
self._pending_lock = pending_lock
self._process = process
self._ready = threading.Event()
self._startup_error: SidecarStartupError | None = None
def wait_ready(self) -> None:
"""Block until startup completes or fails.
Raises:
SidecarStartupError: If the subprocess dies, sends an unexpected
message, or fails to start within the timeout.
"""
self._ready.wait()
if self._startup_error is not None:
raise self._startup_error
def run(self) -> None:
# Phase 1: startup handshake
if not self._wait_for_startup():
self._fail_all_pending(f"Sidecar subprocess failed to start: {self._startup_error}")
return
# Phase 2: response loop
while self._process.is_alive():
try:
raw = self._response_queue.get(timeout=_COLLECTOR_POLL_INTERVAL_SECONDS)
except queue.Empty:
continue
except Exception:
break
if raw is None:
break
try:
self._resolve(pickle.loads(raw))
except Exception:
logger.debug("Failed to deserialize response — skipping")
continue
self._drain_queue()
exitcode = self._process.exitcode
self._fail_all_pending(f"Sidecar subprocess exited unexpectedly (exit code: {exitcode})")
def _wait_for_startup(self) -> bool:
"""Wait for ``_STARTUP_OK``. Returns True on success, False on failure.
Always sets ``_ready`` before returning so ``wait_ready()`` never hangs.
"""
try:
return self._wait_for_startup_inner()
except Exception as e:
# Queue broken (OSError/ValueError from closed queue) or other
# unexpected error. Must still unblock wait_ready().
self._startup_error = SidecarStartupError(
f"Startup handshake failed: {type(e).__name__}: {e}"
)
return False
finally:
self._ready.set()
def _wait_for_startup_inner(self) -> bool:
deadline = time.monotonic() + _STARTUP_TIMEOUT_SECONDS
while time.monotonic() < deadline:
if not self._process.is_alive():
self._startup_error = SidecarStartupError(
f"Sidecar subprocess died before startup (exit code: {self._process.exitcode})"
)
return False
try:
tag, _, _ = self._response_queue.get(timeout=_COLLECTOR_POLL_INTERVAL_SECONDS)
except queue.Empty:
continue
if tag == _STARTUP_OK:
return True
self._startup_error = SidecarStartupError(f"Unexpected startup message: {tag}")
return False
self._startup_error = SidecarStartupError(
f"Sidecar subprocess failed to start within {_STARTUP_TIMEOUT_SECONDS}s"
)
return False
def _resolve(self, msg: tuple[int, Any, BaseException | None]) -> None:
request_id, result, exception = msg
with self._pending_lock:
future = self._pending_futures.pop(request_id, None)
if future is None:
logger.debug(f"Received response for unknown request {request_id}")
return
try:
if exception is not None:
future.set_exception(exception)
else:
future.set_result(result)
except Exception:
logger.debug(f"Could not resolve future for request {request_id} (likely cancelled)")
def _drain_queue(self) -> None:
"""Drain responses that arrived between the last poll and process exit.
Without this, responses enqueued just before the process dies would
never be resolved, leaving their futures hanging until timeout.
"""
while True:
try:
raw = self._response_queue.get_nowait()
except (queue.Empty, OSError, ValueError):
break
if raw is None:
break
try:
self._resolve(pickle.loads(raw))
except Exception:
continue
def _fail_all_pending(self, message: str) -> None:
with self._pending_lock:
futures = list(self._pending_futures.values())
self._pending_futures.clear()
for future in futures:
if not future.done():
# set_exception raises InvalidStateError on a TOCTOU race
# with concurrent cancellation or resolution.
with contextlib.suppress(Exception):
future.set_exception(SidecarDiedError(message))
# ---------------------------------------------------------------------------
# SubprocessSidecar
# ---------------------------------------------------------------------------
class SubprocessSidecar:
"""Runs picklable objects in a dedicated subprocess and routes RPC calls to them.
Use ``create_sidecar_handle()`` to get a ``SidecarHandle`` it manages
the singleton sidecar internally.
Cleanup is automatic: ``__del__`` calls ``_shutdown()`` on GC, and the
parent-death pipe ensures the child self-terminates if the parent is killed.
If the subprocess dies, pending futures fail with ``SidecarDiedError`` and
subsequent ``submit_rpc()`` calls raise immediately. Not picklable.
"""
def __init__(self) -> None:
# Concurrency state — protected by _pending_lock
self._request_id_counter: int = 0
self._pending_futures: dict[int, ConcurrentFuture[Any]] = {}
self._pending_lock: threading.Lock = threading.Lock()
# Subprocess state
self._process: multiprocessing.process.BaseProcess | None = None
self._request_queue: multiprocessing.Queue[Any] | None = None
self._response_queue: multiprocessing.Queue[Any] | None = None
self._parent_conn: multiprocessing.connection.Connection | None = None
self._collector: _ResponseCollector | None = None
self._start_subprocess()
def _start_subprocess(self) -> None:
"""Start the subprocess worker and response collector."""
self._request_queue = _mp_context.Queue()
self._response_queue = _mp_context.Queue()
self._parent_conn, child_conn = _mp_context.Pipe()
self._process = _mp_context.Process(
target=_subprocess_worker,
args=(
self._request_queue,
self._response_queue,
child_conn,
),
daemon=True,
)
self._process.start()
# The collector owns all response_queue reads — including the startup
# handshake. On failure, clean up so repeated failures don't leak FDs.
self._collector = _ResponseCollector(
self._response_queue,
self._pending_futures,
self._pending_lock,
self._process,
)
self._collector.start()
try:
self._collector.wait_ready()
except Exception:
self._shutdown()
raise
logger.debug("SubprocessSidecar started (pid=%s)", self._process.pid)
def register_target(self, pickled_target: bytes) -> SidecarHandle:
"""Register a target in the subprocess. Returns a handle for RPCs.
Blocks the calling thread until the subprocess confirms registration.
Do not call from an asyncio event loop use ``loop.run_in_executor()``
to avoid blocking the loop.
Raises:
SidecarDiedError: If the sidecar subprocess is not running.
Exception: If the target cannot be unpickled in the subprocess.
"""
future = self._submit_rpc(_RegisterTargetRPC(pickled_target=pickled_target))
target_id = future.result()
logger.debug(
"Registered target_id=%d (pickled_target %d bytes)", target_id, len(pickled_target)
)
return SidecarHandle(self, target_id)
def _submit_rpc(
self, rpc: SidecarRPC, *, target_id: int | None = None
) -> ConcurrentFuture[Any]:
"""Submit an RPC to the subprocess and return a Future.
Args:
rpc: The RPC to execute in the subprocess.
target_id: The target to resolve before calling ``rpc.execute(target)``.
``None`` for built-in RPCs that don't need a target.
"""
future: ConcurrentFuture[Any] = ConcurrentFuture()
with self._pending_lock:
process = self._process
request_queue = self._request_queue
if process is None or not process.is_alive():
exitcode = process.exitcode if process is not None else None
raise SidecarDiedError(f"Sidecar subprocess is not running (exit code: {exitcode})")
if request_queue is None:
raise SidecarDiedError("Sidecar subprocess is not running (exit code: None)")
request_id = self._request_id_counter
self._request_id_counter += 1
self._pending_futures[request_id] = future
try:
payload = pickle.dumps((request_id, target_id, rpc))
request_queue.put(payload)
except Exception as e:
with self._pending_lock:
self._pending_futures.pop(request_id, None)
# Guard: a concurrent shutdown() may have already resolved this future.
if not future.done():
future.set_exception(SidecarIPCError(f"Failed to serialize RPC: {e}"))
return future
def _shutdown(self) -> None:
"""Kill the subprocess and release all resources.
All pending futures are failed with ``SidecarDiedError`` before this
method returns. Safe to call multiple times. Called by ``__del__``
on GC and by ``_get_sidecar()`` when replacing a dead sidecar.
"""
# 1. Kill and reap
process = self._process
with contextlib.suppress(Exception):
if process is not None and process.is_alive():
process.kill()
if process is not None:
process.join(timeout=5)
# 2. Null references before failing futures (prevents re-entrant submit)
request_queue = self._request_queue
response_queue = self._response_queue
parent_conn = self._parent_conn
self._process = None
self._request_queue = None
self._response_queue = None
self._parent_conn = None
# 3. Collect under lock, fail outside (prevents deadlock from callbacks)
with self._pending_lock:
futures = list(self._pending_futures.values())
self._pending_futures.clear()
for future in futures:
if not future.done():
with contextlib.suppress(Exception):
future.set_exception(SidecarDiedError("Sidecar subprocess was shut down"))
# 4. Release OS resources
_close_queue(request_queue)
_close_queue(response_queue)
with contextlib.suppress(Exception):
if parent_conn is not None:
parent_conn.close()
def __reduce__(self) -> None: # type: ignore[override]
raise TypeError(
"SubprocessSidecar cannot be pickled. It manages OS-level resources "
"(subprocesses, threads, queues) that cannot be transferred across processes."
)
def __del__(self) -> None:
self._shutdown()
# ---------------------------------------------------------------------------
# Module-level singleton
# ---------------------------------------------------------------------------
_global_sidecar: SubprocessSidecar | None = None
_global_sidecar_lock = threading.Lock()
def _get_sidecar() -> SubprocessSidecar:
"""Return the module-level shared sidecar, creating it if needed.
If the previous sidecar's subprocess has died, a new one is created.
Thread-safe via ``_global_sidecar_lock``.
Note: existing ``SidecarHandle`` instances from the old sidecar will
fail with ``SidecarDiedError`` on next use callers must re-register.
"""
global _global_sidecar
with _global_sidecar_lock:
if (
_global_sidecar is None
or _global_sidecar._process is None
or not _global_sidecar._process.is_alive()
):
old = _global_sidecar
if old is not None:
logger.debug("Sidecar subprocess died, cleaning up before replacement")
old._shutdown()
_global_sidecar = SubprocessSidecar()
return _global_sidecar
def create_sidecar_handle(target: Any) -> SidecarHandle:
"""Register a picklable target in the shared sidecar subprocess.
Pickles ``target``, sends it to the sidecar subprocess, and returns a
``SidecarHandle`` for submitting RPCs. The sidecar singleton is created
on first call and reused thereafter.
Blocks the calling thread until registration completes. Do not call
from an asyncio event loop use ``loop.run_in_executor()`` instead.
Args:
target: Any picklable object to run in the subprocess.
Raises:
RuntimeError: If called from inside the sidecar subprocess.
SidecarStartupError: If the sidecar subprocess fails to start.
SidecarDiedError: If the sidecar subprocess is not running.
Exception: If the target cannot be pickled or unpickled.
"""
if _inside_sidecar:
raise RuntimeError(
"create_sidecar_handle() cannot be called from inside the sidecar subprocess. "
"Daemon processes cannot spawn children."
)
return _get_sidecar().register_target(pickle.dumps(target))

View file

@ -14,6 +14,7 @@ class CreateSessionRequest(StrictBase):
tags: list[str]
user_metadata: dict[str, Any] | None
sdk_version: str
project_id: str | None = None
type: Literal["create_session"] = "create_session"

View file

@ -2,6 +2,7 @@
from __future__ import annotations
import json
import os
import httpx
@ -14,6 +15,20 @@ from tinker import types
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
@pytest.mark.respx(base_url=base_url)
def test_service_client_passes_project_id_on_session_create(respx_mock: MockRouter) -> None:
create_session_route = respx_mock.post("/api/v1/create_session").mock(
return_value=httpx.Response(200, json={"session_id": "test-session-id"})
)
service_client = tinker.ServiceClient(base_url=base_url, project_id="project-123")
service_client.holder.close()
assert create_session_route.called
sent_payload = json.loads(create_session_route.calls[0].request.content.decode())
assert sent_payload["project_id"] == "project-123"
@pytest.mark.respx(base_url=base_url)
async def test_create_training_client_from_state_async(respx_mock: MockRouter) -> None:
"""Test create_training_client_from_state_async uses public endpoint."""

1068
tests/test_sidecar.py Normal file

File diff suppressed because it is too large Load diff

215
uv.lock generated
View file

@ -17,7 +17,7 @@ wheels = [
[[package]]
name = "aiohttp"
version = "3.12.15"
version = "3.13.3"
source = { registry = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/simple" }
dependencies = [
{ name = "aiohappyeyeballs" },
@ -29,95 +29,128 @@ dependencies = [
{ name = "propcache" },
{ name = "yarl" },
]
sdist = { url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2" }
sdist = { url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88" }
wheels = [
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b6fc902bff74d9b1879ad55f5404153e2b33a82e72a95c89cec5eb6cc9e92fbc" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:098e92835b8119b54c693f2f88a1dec690e20798ca5f5fe5f0520245253ee0af" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:40b3fee496a47c3b4a39a731954c06f0bd9bd3e8258c059a4beb76ac23f8e421" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ce13fcfb0bb2f259fb42106cdc63fa5515fb85b7e87177267d89a771a660b79" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3beb14f053222b391bf9cf92ae82e0171067cc9c8f52453a0f1ec7c37df12a77" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c39e87afe48aa3e814cac5f535bc6199180a53e38d3f51c5e2530f5aa4ec58c" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f1b4ce5bc528a6ee38dbf5f39bbf11dd127048726323b72b8e85769319ffc4" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1004e67962efabbaf3f03b11b4c43b834081c9e3f9b32b16a7d97d4708a9abe6" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8faa08fcc2e411f7ab91d1541d9d597d3a90e9004180edb2072238c085eac8c2" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fe086edf38b2222328cdf89af0dde2439ee173b8ad7cb659b4e4c6f385b2be3d" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:79b26fe467219add81d5e47b4a4ba0f2394e8b7c7c3198ed36609f9ba161aecb" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b761bac1192ef24e16706d761aefcb581438b34b13a2f069a6d343ec8fb693a5" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e153e8adacfe2af562861b72f8bc47f8a5c08e010ac94eebbe33dc21d677cd5b" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fc49c4de44977aa8601a00edbf157e9a421f227aa7eb477d9e3df48343311065" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2776c7ec89c54a47029940177e75c8c07c29c66f73464784971d6a81904ce9d1" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp310-cp310-win32.whl", hash = "sha256:2c7d81a277fa78b2203ab626ced1487420e8c11a8e373707ab72d189fcdad20a" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp310-cp310-win_amd64.whl", hash = "sha256:83603f881e11f0f710f8e2327817c82e79431ec976448839f3cd05d7afe8f830" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d3ce17ce0220383a0f9ea07175eeaa6aa13ae5a41f30bc61d84df17f0e9b1117" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:010cc9bbd06db80fe234d9003f67e97a10fe003bfbedb40da7d71c1008eda0fe" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f9d7c55b41ed687b9d7165b17672340187f87a773c98236c987f08c858145a9" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc4fbc61bb3548d3b482f9ac7ddd0f18c67e4225aaa4e8552b9f1ac7e6bda9e5" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7fbc8a7c410bb3ad5d595bb7118147dfbb6449d862cc1125cf8867cb337e8728" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74dad41b3458dbb0511e760fb355bb0b6689e0630de8a22b1b62a98777136e16" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6f0af863cf17e6222b1735a756d664159e58855da99cfe965134a3ff63b0b0" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5b7fe4972d48a4da367043b8e023fb70a04d1490aa7d68800e465d1b97e493b" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6443cca89553b7a5485331bc9bedb2342b08d073fa10b8c7d1c60579c4a7b9bd" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c5f40ec615e5264f44b4282ee27628cea221fcad52f27405b80abb346d9f3f8" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2abbb216a1d3a2fe86dbd2edce20cdc5e9ad0be6378455b05ec7f77361b3ab50" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:db71ce547012a5420a39c1b744d485cfb823564d01d5d20805977f5ea1345676" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ced339d7c9b5030abad5854aa5413a77565e5b6e6248ff927d3e174baf3badf7" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7c7dd29c7b5bda137464dc9bfc738d7ceea46ff70309859ffde8c022e9b08ba7" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:421da6fd326460517873274875c6c5a18ff225b40da2616083c5a34a7570b685" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp311-cp311-win32.whl", hash = "sha256:4420cf9d179ec8dfe4be10e7d0fe47d6d606485512ea2265b0d8c5113372771b" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp311-cp311-win_amd64.whl", hash = "sha256:edd533a07da85baa4b423ee8839e3e91681c7bfa19b04260a469ee94b778bf6d" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp312-cp312-win32.whl", hash = "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp312-cp312-win_amd64.whl", hash = "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp313-cp313-win32.whl", hash = "sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp313-cp313-win_amd64.whl", hash = "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:691d203c2bdf4f4637792efbbcdcd157ae11e55eaeb5e9c360c1206fb03d4d98" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e995e1abc4ed2a454c731385bf4082be06f875822adc4c6d9eaadf96e20d406" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bd44d5936ab3193c617bfd6c9a7d8d1085a8dc8c3f44d5f1dcf554d17d04cf7d" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46749be6e89cd78d6068cdf7da51dbcfa4321147ab8e4116ee6678d9a056a0cf" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0c643f4d75adea39e92c0f01b3fb83d57abdec8c9279b3078b68a3a52b3933b6" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a23918fedc05806966a2438489dcffccbdf83e921a1170773b6178d04ade142" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:74bdd8c864b36c3673741023343565d95bfbd778ffe1eb4d412c135a28a8dc89" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a146708808c9b7a988a4af3821379e379e0f0e5e466ca31a73dbdd0325b0263" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7011a70b56facde58d6d26da4fec3280cc8e2a78c714c96b7a01a87930a9530" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3bdd6e17e16e1dbd3db74d7f989e8af29c4d2e025f9828e6ef45fbdee158ec75" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:57d16590a351dfc914670bd72530fd78344b885a00b250e992faea565b7fdc05" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:bc9a0f6569ff990e0bbd75506c8d8fe7214c8f6579cca32f0546e54372a3bb54" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:536ad7234747a37e50e7b6794ea868833d5220b49c92806ae2d7e8a9d6b5de02" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f0adb4177fa748072546fb650d9bd7398caaf0e15b370ed3317280b13f4083b0" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:14954a2988feae3987f1eb49c706bff39947605f4b6fa4027c1d75743723eb09" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp39-cp39-win32.whl", hash = "sha256:b784d6ed757f27574dca1c336f968f4e81130b27595e458e69457e6878251f5d" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.12.15-cp39-cp39-win_amd64.whl", hash = "sha256:86ceded4e78a992f835209e236617bffae649371c4a50d5e5a3987f237db84b8" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31a83ea4aead760dfcb6962efb1d861db48c34379f2ff72db9ddddd4cda9ea2e" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:988a8c5e317544fdf0d39871559e67b6341065b87fceac641108c2096d5506b7" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b174f267b5cfb9a7dba9ee6859cecd234e9a681841eb85068059bc867fb8f02" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:947c26539750deeaee933b000fb6517cc770bbd064bad6033f1cff4803881e43" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9ebf57d09e131f5323464bd347135a88622d1c0976e88ce15b670e7ad57e4bd6" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4ae5b5a0e1926e504c81c5b84353e7a5516d8778fbbff00429fe7b05bb25cbce" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ba0eea45eb5cc3172dbfc497c066f19c41bac70963ea1a67d51fc92e4cf9a80" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bae5c2ed2eae26cc382020edad80d01f36cb8e746da40b292e68fec40421dc6a" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a60e60746623925eab7d25823329941aee7242d559baa119ca2b253c88a7bd6" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e50a2e1404f063427c9d027378472316201a2290959a295169bcf25992d04558" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:9a9dc347e5a3dc7dfdbc1f82da0ef29e388ddb2ed281bfce9dd8248a313e62b7" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b46020d11d23fe16551466c77823df9cc2f2c1e63cc965daf67fa5eec6ca1877" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:69c56fbc1993fa17043e24a546959c0178fe2b5782405ad4559e6c13975c15e3" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b99281b0704c103d4e11e72a76f1b543d4946fea7dd10767e7e1b5f00d4e5704" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:40c5e40ecc29ba010656c18052b877a1c28f84344825efa106705e835c28530f" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp39-cp39-win32.whl", hash = "sha256:56339a36b9f1fc708260c76c87e593e2afb30d26de9ae1eb445b5e051b98a7a1" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/aiohttp/aiohttp-3.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:c6b8568a3bb5819a0ad087f16d40e5a3fb6099f39ea1d5625a3edc1e923fc538" },
]
[[package]]
name = "aiosignal"
version = "1.4.0"
@ -361,15 +394,15 @@ wheels = [
[[package]]
name = "h2"
version = "4.2.0"
version = "4.3.0"
source = { registry = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/simple" }
dependencies = [
{ name = "hpack" },
{ name = "hyperframe" },
]
sdist = { url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/h2/h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f" }
sdist = { url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/h2/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1" }
wheels = [
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/h2/h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0" },
{ url = "https://us-south1-python.pkg.dev/iterate-images/pypi-internal/h2/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd" },
]
[[package]]