mirror of
https://github.com/thinking-machines-lab/tinker.git
synced 2026-04-19 12:58:01 +00:00
Sync contents
This commit is contained in:
parent
77cc8a0810
commit
35a77e79fe
15 changed files with 2186 additions and 152 deletions
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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__(
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
||||
|
|
|
|||
|
|
@ -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,23 +137,29 @@ class _APIFuture(APIFuture[T]): # pyright: ignore[reportUnusedClass]
|
|||
user_error = is_user_error(e)
|
||||
if telemetry := self.get_telemetry():
|
||||
current_time = time.time()
|
||||
event_data: dict[str, object] = {
|
||||
"request_id": self.request_id,
|
||||
"request_type": self.request_type,
|
||||
"status_code": e.status_code,
|
||||
"exception": str(e),
|
||||
"should_retry": should_retry,
|
||||
"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={
|
||||
"request_id": self.request_id,
|
||||
"request_type": self.request_type,
|
||||
"status_code": e.status_code,
|
||||
"exception": str(e),
|
||||
"should_retry": should_retry,
|
||||
"is_user_error": user_error,
|
||||
"iteration": iteration,
|
||||
"elapsed_time": current_time - start_time,
|
||||
},
|
||||
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:
|
||||
return await asyncio.wait_for(self._future, timeout)
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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,16 +185,31 @@ 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(
|
||||
self._start_heartbeat()
|
||||
).result()
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
759
src/tinker/lib/sidecar.py
Normal 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))
|
||||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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
1068
tests/test_sidecar.py
Normal file
File diff suppressed because it is too large
Load diff
215
uv.lock
generated
215
uv.lock
generated
|
|
@ -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]]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue