diff --git a/.github/scripts/run_examples.sh b/.github/scripts/run_examples.sh index 36716029a5..f535b7cdf2 100755 --- a/.github/scripts/run_examples.sh +++ b/.github/scripts/run_examples.sh @@ -4,7 +4,7 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" PID_FILE="$ROOT/.tmp/examples-auto-run.pid" LOG_DIR="$ROOT/.tmp/examples-start-logs" -DEFAULT_UV_EXTRAS="litellm any-llm sqlalchemy redis blaxel modal runloop temporal" +DEFAULT_UV_EXTRAS="litellm any-llm sqlalchemy redis blaxel modal temporal" build_uv_prefix() { UV_RUN=(uv run) @@ -169,7 +169,7 @@ Commands: Environment overrides: EXAMPLES_INTERACTIVE_MODE (default auto) EXAMPLES_INCLUDE_SERVER/INTERACTIVE/AUDIO/EXTERNAL (defaults: 0/1/0/0) - EXAMPLES_UV_EXTRAS (default: litellm any-llm sqlalchemy redis blaxel modal runloop temporal; set empty to disable) + EXAMPLES_UV_EXTRAS (default: litellm any-llm sqlalchemy redis blaxel modal temporal; set empty to disable) APPLY_PATCH_AUTO_APPROVE, AUTO_APPROVE_MCP (default 1 in auto mode) EOF } diff --git a/.github/scripts/run_integration_tests.py b/.github/scripts/run_integration_tests.py index 90627d1386..b874566c6d 100644 --- a/.github/scripts/run_integration_tests.py +++ b/.github/scripts/run_integration_tests.py @@ -486,7 +486,8 @@ def main() -> None: print(f"[integration] wheel={wheel.name} sdist={sdist.name} profile={args.profile}") if args.profile == "mcp-v1": - for mcp_version in ("1.19.0", "1.29.0"): + # MCP 1.30.0 is the patched v1 floor for GHSA-rwrf-2pqf-9j8j. + for mcp_version in ("1.30.0",): environment_kind = f"mcp-v1-{mcp_version}" python = create_environment( environment_kind, diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e437e87ffb..2ea7544adf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -118,7 +118,8 @@ jobs: tests: runs-on: ubuntu-latest - timeout-minutes: 10 + # Python 3.14 also runs coverage and installs Chromium with system dependencies. + timeout-minutes: ${{ matrix.python-version == '3.14' && 30 || 10 }} strategy: fail-fast: false matrix: diff --git a/docs/human_in_the_loop.md b/docs/human_in_the_loop.md index 07450639fe..047830fdfb 100644 --- a/docs/human_in_the_loop.md +++ b/docs/human_in_the_loop.md @@ -49,12 +49,22 @@ For `ComputerTool`, use `on_safety_check` to accept or reject safety checks retu ## How the approval flow works 1. When the model emits a tool call, the runner evaluates its approval rule (`needs_approval`, `require_approval`, or the hosted MCP equivalent). -2. If an approval decision for that tool call is already stored in the [`RunContextWrapper`][agents.run_context.RunContextWrapper], the runner proceeds without prompting. Per-call approvals are scoped to the specific call ID; pass `always_approve=True` or `always_reject=True` to persist the same decision for future calls to the same tool identity during the rest of the run. +2. If an approval decision for that tool call is already stored in the [`RunContextWrapper`][agents.run_context.RunContextWrapper], the runner proceeds without prompting. Per-call approvals are scoped to the specific call ID; pass `always_approve=True` or `always_reject=True` to persist the same decision for future calls to the same tool identity. 3. If the approval rule requires approval and no decision for that tool call is stored, execution pauses, and `RunResult.interruptions` (or `RunResultStreaming.interruptions`) contains [`ToolApprovalItem`][agents.items.ToolApprovalItem] entries with details such as `agent.name`, `tool_name`, and `arguments`. This includes approvals raised after a handoff or inside nested `Agent.as_tool()` executions. 4. Convert the result to a `RunState` with `result.to_state()`, call `state.approve(...)` or `state.reject(...)`, and then resume with `Runner.run(agent, state)` or `Runner.run_streamed(agent, state)`, where `agent` is the original top-level agent for the run. 5. The resumed run continues where it left off and will re-enter this flow if new approvals are needed. -Sticky decisions created with `always_approve=True` or `always_reject=True` are stored in the run state, so they survive `state.to_string()` / `RunState.from_string(...)` and `state.to_json()` / `RunState.from_json(...)` when you resume the same paused run later. +Sticky decisions created with `always_approve=True` or `always_reject=True` remain active while the approval-bearing `RunContextWrapper` is reused, including across separate `Runner.run(...)` or `Runner.run_streamed(...)` calls. Passing a plain application context object to a new run creates a fresh wrapper; reusing that object alone does not carry approvals forward. `RunState` also stores sticky decisions, so they survive `state.to_string()` / `RunState.from_string(...)` and `state.to_json()` / `RunState.from_json(...)` when you resume the paused run later. + +For function tools, sticky decisions are scoped to the agent that requested approval. A different agent's function tool does not inherit a decision merely because the tool has the same name. + +!!! warning "Persistent shell and file-edit decisions are not agent-scoped" + + For local [`ShellTool`][agents.tool.ShellTool] and [`ApplyPatchTool`][agents.tool.ApplyPatchTool], sticky decisions match the tool type and name, not the owning agent, executor, or editor. When agents share approval state, an `always_approve=True` decision can apply to another agent's tool of the same type and name after a handoff, even when the executors or editors have different permissions. The default names are `shell` and `apply_patch`, respectively. The SDK does not prompt again or re-evaluate the destination tool's `needs_approval` policy or `on_approval` callback when that sticky approval applies. `always_reject=True` decisions are shared in the same way. + + When executors or editors have different permissions, use per-call decisions: call `state.approve(interruption)` or `state.reject(interruption)`, leaving `always_approve` and `always_reject` at their default `False` values. To require a human decision for every new shell or file-edit call, also set `needs_approval=True` and omit `on_approval`. A per-call decision does not clear an existing sticky decision. + + When starting a new run with different execution permissions, use a fresh `RunContextWrapper` rather than reusing a wrapper that holds approvals. Resuming an existing `RunState` preserves its approvals, even if you pass a new context object or wrapper; a context override does not reset approval state. For approval requests from [`HostedMCPTool`][agents.tool.HostedMCPTool], the Agents SDK identifies a sticky tool decision by the combination of `server_label` and tool name. An always-approve decision for `lookup_account` on one hosted MCP server does not approve a tool with the same name on another server. The Agents SDK persists an always-approve or always-reject decision only when the hosted MCP approval request includes both non-empty identity fields. @@ -182,10 +192,10 @@ To use streaming in a run that may pause for approvals, call `Runner.run_streame - **Streaming approvals**: `examples/agent_patterns/human_in_the_loop_stream.py` shows how to drain `stream_events()` and then approve pending tool calls before resuming with `Runner.run_streamed(agent, state)`. - **Custom rejection text**: `examples/agent_patterns/human_in_the_loop_custom_rejection.py` shows how to combine run-level `tool_error_formatter` with per-call `rejection_message` overrides when approvals are rejected. - **Agent as tool approvals**: `Agent.as_tool(..., needs_approval=...)` applies the same interruption flow when delegated agent tasks need review. Nested interruptions still surface on the outer run, so resume the original top-level agent rather than the nested one. -- **Local shell and apply_patch tools**: `ShellTool` and `ApplyPatchTool` also support `needs_approval`. Use `state.approve(interruption, always_approve=True)` or `state.reject(..., always_reject=True)` to cache the decision for future calls to that tool during the rest of the run. To resolve approval inside a callback, provide `on_approval`; `examples/tools/shell.py` demonstrates an interactive callback that prompts the operator by default. For an automatic policy that rejects every shell call, set `needs_approval=True` and `on_approval=lambda _context, _item: {"approve": False, "reason": "Disabled by policy"}` on `ShellTool`. To let the application review a paused run instead, handle interruptions (see `examples/tools/shell_human_in_the_loop.py`). Hosted shell environments do not support `needs_approval` or `on_approval`; see the [tools guide](tools.md). +- **Local shell and apply_patch tools**: `ShellTool` and `ApplyPatchTool` also support `needs_approval`. Use `state.approve(interruption)` or `state.reject(interruption)` for per-call decisions. The `always_approve=True` and `always_reject=True` options persist decisions in the `RunContextWrapper` and `RunState`; review the [approval scope warning](#how-the-approval-flow-works) before using these options. To resolve approval inside a callback, provide `on_approval`; `examples/tools/shell.py` demonstrates an interactive callback that prompts the operator by default. To automatically reject shell calls that have no existing approval decision, set `needs_approval=True` and `on_approval=lambda _context, _item: {"approve": False, "reason": "Disabled by policy"}` on `ShellTool`. To let the application review a paused run instead, handle interruptions (see `examples/tools/shell_human_in_the_loop.py`). Hosted shell environments do not support `needs_approval` or `on_approval`; see the [tools guide](tools.md). - **Local MCP servers**: Use `require_approval` on `MCPServerStdio` / `MCPServerSse` / `MCPServerStreamableHttp` to gate MCP tool calls (see `examples/mcp/get_all_mcp_tools_example/main.py` and `examples/mcp/tool_filter_example/main.py`). - **Hosted MCP servers**: Set `tool_config={"require_approval": "always"}` on `HostedMCPTool` to force HITL, optionally providing `on_approval_request` to auto-approve or reject (see `examples/hosted_mcp/human_in_the_loop.py` and `examples/hosted_mcp/on_approval.py`). Use `"never"` for trusted servers (`examples/hosted_mcp/simple.py`). -- **Sessions and memory**: Pass a session to `Runner.run` so approvals and conversation history survive multiple turns. SQLite and OpenAI Conversations session variants are in `examples/memory/memory_session_hitl_example.py` and `examples/memory/openai_session_hitl_example.py`. +- **Sessions and memory**: Pass a session to `Runner.run` to preserve conversation history across turns, and use `RunState` to preserve approval decisions when resuming a paused run. SQLite and OpenAI Conversations session variants are in `examples/memory/memory_session_hitl_example.py` and `examples/memory/openai_session_hitl_example.py`. - **Realtime agents**: The realtime demo exposes WebSocket messages that approve or reject tool calls via `approve_tool_call` / `reject_tool_call` on the `RealtimeSession` (see `examples/realtime/app/server.py` for the server-side handlers and [Realtime guide](realtime/guide.md#tool-approvals) for the API surface). ## Long-running approvals @@ -209,6 +219,14 @@ The [server-side approval example](https://github.com/openai/openai-agents-pytho Replacing `context` with `context_override`, setting `strict_context=True`, or removing only the serialized approval records does not make an untrusted snapshot safe. Other fields still control resumed execution. If an application transports the complete snapshot through a client, the application must verify its integrity, bind it to the authorized user and run, and prevent replay before deserialization. Such verification does not encrypt the snapshot or hide its contents from the client. +### Recovering after a crash + +Approval permits a tool call to execute; it does not guarantee exactly-once execution. If a tool performs an external action and the process stops before the application stores the updated run state, resuming from the older approved snapshot can execute the tool again. For example, a payment can complete even though the saved snapshot has no record of the payment result. + +For tools with consequential side effects, the application should assign a stable idempotency key to each logical action and pass that key to a destination that atomically prevents duplicate effects. Reuse the same key and action parameters across retries, and respect the destination's key retention period and retry rules. An approval decision or a local completion flag alone cannot make the external action and the saved run state one atomic operation. + +If destination-enforced idempotency is unavailable or no longer valid, reconcile the outcome with the destination before retrying. A missing result in an ordinary lookup is not proof that retrying is safe: reads can lag, and an earlier request can still complete. Retry only after authoritative reconciliation establishes that no earlier attempt performed the action or can still perform it. If the outcome remains uncertain, the application should record it as unknown and avoid automatic replay. + ### Serialization options Useful serialization options: diff --git a/docs/mcp.md b/docs/mcp.md index 8191d0576e..84075fd08e 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -250,13 +250,16 @@ asyncio.run(main()) The constructor accepts additional options: -- `client_session_timeout_seconds` controls MCP ClientSession read timeouts. Positive finite values representable by `datetime.timedelta` and at least one microsecond set a finite timeout; `None` and `0` disable it. Other values are rejected when the server is constructed. +- `client_session_timeout_seconds` controls MCP ClientSession read timeouts and defaults to `5` seconds on `MCPServerStdio`, `MCPServerSse`, and `MCPServerStreamableHttp`. Positive finite values representable by `datetime.timedelta` and at least one microsecond set a finite timeout; `None` and `0` disable it. Other values are rejected when the server is constructed. - `use_structured_content` toggles whether `tool_result.structured_content` is preferred over textual output. - `max_retry_attempts` and `retry_backoff_seconds_base` add automatic retries for `list_tools()` and `call_tool()`. +- `retry_backoff_seconds_max` caps the delay between retries in seconds; `None` (the default) leaves exponential backoff uncapped. +- `max_list_pages` limits successful pages per automatic `list_tools()` or `list_prompts()` call. `None` (the default) applies no page limit. With a positive integer limit, completion on the final permitted page succeeds; further continuation raises `UserError` without returning or caching a partial list. - `tool_filter` lets you expose only a subset of tools (see [Tool filtering](#tool-filtering)). - `require_approval` enables human-in-the-loop approval policies on local MCP tools. - `failure_error_function` customizes model-visible MCP tool failure messages; set it to `None` to raise errors instead. - `tool_meta_resolver` injects per-call MCP `_meta` payloads before `call_tool()`. +- `custom_data_extractor` receives `MCPToolCustomDataContext` and returns SDK-only custom data for emitted MCP tool output items. ### Approval policies for local MCP servers @@ -366,6 +369,7 @@ async with MCPServerStdio( "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", str(samples_dir)], }, + client_session_timeout_seconds=30, ) as server: agent = Agent( name="Assistant", @@ -376,6 +380,12 @@ async with MCPServerStdio( print(result.final_output) ``` +!!! tip "Package runner cold starts" + + Package runners such as `npx -y` and `uvx` may download packages before the MCP server starts. Install the package before connecting to avoid spending connection setup time on these downloads. + + The example increases `client_session_timeout_seconds` to allow more time for initialization and subsequent session requests. With MCP Python SDK v1, this can also accommodate package downloads during initialization. With MCP Python SDK v2, the initial `server/discover` request has a separate timeout that this setting does not change. Preinstall the package for slow v2 cold starts; increasing the session timeout alone does not control every stage of connection setup. + ## 5. MCP server manager When you have multiple MCP servers, use `MCPServerManager` to connect them up front and expose the successfully connected subset of those servers to your agents. See the [MCPServerManager API reference](ref/mcp/manager.md) for constructor options and reconnect behavior. diff --git a/docs/ref/extensions/sandbox/runloop/mounts.md b/docs/ref/extensions/sandbox/runloop/mounts.md deleted file mode 100644 index fe5b77c1d7..0000000000 --- a/docs/ref/extensions/sandbox/runloop/mounts.md +++ /dev/null @@ -1,3 +0,0 @@ -# `Mounts` - -::: agents.extensions.sandbox.runloop.mounts diff --git a/docs/ref/extensions/sandbox/runloop/sandbox.md b/docs/ref/extensions/sandbox/runloop/sandbox.md deleted file mode 100644 index 89ab3401d7..0000000000 --- a/docs/ref/extensions/sandbox/runloop/sandbox.md +++ /dev/null @@ -1,3 +0,0 @@ -# `Sandbox` - -::: agents.extensions.sandbox.runloop.sandbox diff --git a/docs/sandbox/clients.md b/docs/sandbox/clients.md index 7a9d528ec2..9248ddc814 100644 --- a/docs/sandbox/clients.md +++ b/docs/sandbox/clients.md @@ -149,7 +149,6 @@ For provider-specific setup notes and links for the checked-in extension example | `DaytonaSandboxClient` | `openai-agents[daytona]` | [Daytona runner](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/extensions/daytona/daytona_runner.py) | | `E2BSandboxClient` | `openai-agents[e2b]` | [E2B runner](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/extensions/e2b_runner.py) | | `ModalSandboxClient` | `openai-agents[modal]` | [Modal runner](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/extensions/modal_runner.py) | -| `RunloopSandboxClient` | `openai-agents[runloop]` | [Runloop runner](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/extensions/runloop/runner.py) | | `VercelSandboxClient` | `openai-agents[vercel]` | [Vercel runner](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/extensions/vercel_runner.py) | @@ -182,7 +181,6 @@ Hosted sandbox clients expose provider-specific mount strategies. Choose the bac | `BlaxelSandboxClient` | Supports cloud bucket mounts by pairing `BlaxelCloudBucketMountStrategy` with an `S3Mount`, `R2Mount`, or `GCSMount` entry. Also supports persistent Blaxel Drives with `BlaxelDriveMount` and `BlaxelDriveMountStrategy`, both available from `agents.extensions.sandbox.blaxel`. | | `DaytonaSandboxClient` | Supports mounting cloud storage through `rclone` by using `DaytonaCloudBucketMountStrategy`; use it with `S3Mount`, `GCSMount`, `R2Mount`, `AzureBlobMount`, and `BoxMount`. | | `E2BSandboxClient` | Supports mounting cloud storage through `rclone` by using `E2BCloudBucketMountStrategy`; use it with `S3Mount`, `GCSMount`, `R2Mount`, `AzureBlobMount`, and `BoxMount`. | -| `RunloopSandboxClient` | Supports mounting cloud storage through `rclone` by using `RunloopCloudBucketMountStrategy`; use it with `S3Mount`, `GCSMount`, `R2Mount`, `AzureBlobMount`, and `BoxMount`. | | `VercelSandboxClient` | Supports create-time-only S3 and S3-compatible bucket mounts by pairing `VercelCloudBucketMountStrategy` with an `S3Mount` entry; mounted sessions cannot be resumed, and inline credentials require `allow_s3_credential_exposure=True`. | @@ -217,9 +215,24 @@ The table below summarizes which remote storage entries each backend can mount d | `BlaxelSandboxClient` | ✓ | ✓ | ✓ | - | - | - | | `DaytonaSandboxClient` | ✓ | ✓ | ✓ | ✓ | ✓ | - | | `E2BSandboxClient` | ✓ | ✓ | ✓ | ✓ | ✓ | - | -| `RunloopSandboxClient` | ✓ | ✓ | ✓ | ✓ | ✓ | - | | `VercelSandboxClient` | ✓ | - | - | - | - | - | For more runnable examples, browse [examples/sandbox/](https://github.com/openai/openai-agents-python/tree/main/examples/sandbox) for local, coding, memory, handoff, and agent-composition patterns, and [examples/sandbox/extensions/](https://github.com/openai/openai-agents-python/tree/main/examples/sandbox/extensions) for hosted sandbox clients. + +## External providers + +Applications can supply a sandbox client from a separately installed package through `SandboxRunConfig(client=...)`. See [Build a sandbox provider](custom-providers.md) for the authoring contract and a runnable external-package example. + +### Community-maintained providers + +These sandbox clients are maintained outside this repository and published as their own packages. Install the package, then pass the client to `SandboxRunConfig(client=...)`; the `SandboxAgent` definition stays the same. + +
+ +| Provider | Install | Client | Links | +| --- | --- | --- | --- | +| Databricks | `databricks-openai[sandbox]` | `DatabricksSandboxClient` | [PyPI](https://pypi.org/project/databricks-openai/) / [Source](https://github.com/databricks/databricks-ai-bridge/tree/main/integrations/openai) | + +
diff --git a/docs/sandbox/custom-providers.md b/docs/sandbox/custom-providers.md new file mode 100644 index 0000000000..2ab31d54e2 --- /dev/null +++ b/docs/sandbox/custom-providers.md @@ -0,0 +1,115 @@ +# Build a sandbox provider + +You can distribute a sandbox client in your own Python package. Applications import your client and pass an instance to `SandboxRunConfig(client=...)`. The SDK does not need to import your package, add it to `agents.extensions.sandbox`, or discover it through Python entry points. + +This guide describes sandbox execution providers, not model providers. The outer `Runner` still owns model turns, approvals, handoffs, and run history. Your client and session implement the execution environment. + +## Start with a working external package + +The [external provider example](https://github.com/openai/openai-agents-python/tree/main/examples/sandbox/external_provider) is a separately installable distribution with its own `pyproject.toml`, import package, and runnable demo. It implements `ExampleDockerClient` by composing the SDK's `DockerSandboxClient`. The example demonstrates the packaging and client boundary using real Docker execution; it is not a new container engine or a published third-party service. + +After installing the example package, construct the client in application code: + +```python +import docker +from example_docker_provider import ExampleDockerClient, ExampleDockerOptions + +from agents import RunConfig +from agents.sandbox import SandboxRunConfig +from agents.sandbox.sandboxes.docker import DockerSandboxClient + +connection = docker.from_env() +client = ExampleDockerClient(DockerSandboxClient(connection)) +run_config = RunConfig( + sandbox=SandboxRunConfig( + client=client, + options=ExampleDockerOptions(image="python:3.14-slim"), + ), +) +# Pass run_config to Runner.run(...). +# Close connection after all runs using this client have finished. +``` + +The [demo](https://github.com/openai/openai-agents-python/blob/main/examples/sandbox/external_provider/src/example_docker_provider/demo.py) shows the complete `try`/`finally` connection lifecycle. It uses a real container with `ScriptedModel`, so the demo needs no model API key or model request. The runner creates and cleans up the sandbox session. The application closes the Docker connection that the application created. + +The example requires a typed `ExampleDockerOptions` instance. Dictionary options passed to `SandboxRunConfig` are parsed according to the client’s backend ID, which remains `docker` here; the adapter rejects the resulting Docker options before creating a session. It also rejects unknown fields on its own options model. An adapter must preserve accepted settings or reject unsupported inputs before delegation, especially settings that control isolation. Use `DockerSandboxClient` directly for Docker networking and other advanced options. + +Use composition when an existing backend already implements your runtime. Return the delegate's `SandboxSession` unchanged, and delegate serialization and resume to the same backend. The example retains `backend_id = "docker"` and Docker's state type because the underlying session remains a Docker session. It does not introduce an independent persisted state format or claim additional isolation. + +## Implement a new execution runtime + +A backend for a different runtime, such as a micro-VM service, needs its own client and session implementation. The relevant interfaces are exported from `agents.sandbox.session`: + +| Interface | Responsibility | +| --- | --- | +| `BaseSandboxClient` | Create, resume, and delete sessions; serialize and deserialize backend state. | +| `BaseSandboxClientOptions` | Validate provider-specific creation options and support typed JSON parsing. | +| `BaseSandboxSession` | Execute commands, access files, manage the runtime lifecycle, and persist or hydrate workspace contents. | +| `SandboxSessionState` | Represent the provider identity and workspace state needed for resume. | +| `SandboxSession` | Wrap a backend session with the SDK's instrumentation and dependency lifecycle. | + +Implement `create`, `resume`, `delete`, and `deserialize_session_state` on your client. Give a genuinely new runtime a distinct, stable `backend_id`; the runner checks this identity when restoring its saved sandbox state. Keep credentials, SDK connections, and trusted executable selection on the live client rather than in model-visible options or serialized state. + +`create` receives `manifest`, `snapshot`, and your `options` object. Preserve their meaning instead of silently ignoring inputs. Validate unsupported options before allocating resources where possible, and clean up resources if creation fails. Return an instrumented `SandboxSession`, not a bare backend session. Existing clients use the base client's `_wrap_session` hook to preserve dependency cloning and instrumentation; an adapter that delegates to an existing client should reuse the returned wrapper instead of wrapping it again. + +A new `BaseSandboxSession` must implement the abstract command, file, status, and archive methods: `_exec_internal`, `read`, `write`, `running`, `persist_workspace`, and `hydrate_workspace`. Abstract-method completeness alone does not make a working backend. The session must also implement the appropriate startup and shutdown hooks for its runtime, satisfy the commands used by enabled capabilities and inherited helpers, and enforce its filesystem and process boundaries. Consult the [session implementation](https://github.com/openai/openai-agents-python/blob/main/src/agents/sandbox/session/base_sandbox_session.py) and a comparable backend before choosing which operations to inherit. + +Underscored base-class hooks are implementation coupling: pin and test the SDK versions your package supports. Do not copy private helpers from another backend or assume that changing only its command executor gives your runtime the same filesystem, persistence, or isolation semantics. In particular, host filesystem access inherited from a local backend needs a separate trust-boundary assessment for a VM backend. + +State the capabilities your backend supports. Reject unsupported PTY sessions, users, exposed ports, mounts, or manifest entries explicitly instead of executing them on the host as a fallback. Translate provider failures into the SDK's sandbox error types while preserving useful error codes and retryability without exposing credentials or raw sensitive responses. + +## Registration happens when types are imported + +Client selection and serialized-type registration are separate operations: + +- Applications select the client by passing an instance to `SandboxRunConfig`. There is no global client factory or entry-point discovery step. +- `BaseSandboxClientOptions` subclasses register automatically from a non-empty `type` default when Python defines the class. +- `SandboxSessionState` subclasses register automatically when their `type` field has a `Literal` annotation and a non-empty string default. + +For a new runtime, define stable type identifiers owned by your package. The following declarations illustrate the type-registration mechanism; they do not implement a runnable backend: + +```python +from typing import Literal + +from agents.sandbox.session import BaseSandboxClientOptions, SandboxSessionState + + +class AcmeOptions(BaseSandboxClientOptions): + type: Literal["acme_microvm"] = "acme_microvm" + image: str + + +class AcmeState(SandboxSessionState): + type: Literal["acme_microvm"] = "acme_microvm" + provider_session_id: str +``` + +Import these classes in every process that parses their serialized values, including resume workers. For example, importing the external example package makes `BaseSandboxClientOptions.parse({"type": "example_docker", "image": "python:3.14-slim"})` return `ExampleDockerOptions`. Installing a distribution alone does not execute those imports. Do not modify the registries directly. Choose identifiers that do not collide with SDK or other package types; do not rely on collision handling as validation of your package's identity. + +The external Docker example needs only a new options type. Its session state remains `DockerSandboxSessionState`, which the delegate imports and understands. A package's distribution name, import name, options type, and execution backend ID serve different purposes and need not match. + +## Preserve lifecycle and resume semantics + +When an application supplies `client=`, the runner owns the session it creates or resumes. The runner starts the session, uses its capabilities, persists snapshot-backed workspace contents during cleanup, and invokes the backend cleanup paths. Your client must release resources it owns without deleting caller-owned resources. Cleanup must tolerate partial startup and repeated calls. + +When an application supplies a live `session=`, the application owns that session. The runner can use the session but does not take over its full lifetime. Document how callers close sessions and how callers close live provider connections. See [session lifecycle](guide.md#sandbox-lifecycle) for both usage patterns. + +Provider session state and workspace snapshots are different. State identifies or describes an execution environment that can be resumed. A snapshot contains workspace data that can seed a new environment. Resume should reconnect to the existing environment when possible, or reconstruct it from the saved snapshot if your backend supports that behavior. Do not overwrite an injected or resumed workspace with fresh creation inputs. + +Use the client's serialization/deserialization path when restoring a backend session. Generic `SandboxSessionState.parse` selects a registered type; it does not authenticate a remote session or authorize host paths, credentials, mount access, or network policy. Preserve the SDK's handling of persisted grants and mount authority, and require trusted application configuration to rebind authority when necessary. Document which serialized fields survive across package upgrades, and test that supported saved states remain usable. + +For per-command VMs, specify which files survive the next command, whether guest-installed packages persist, and whether background processes can survive. A warm VM image and an SDK workspace snapshot are not interchangeable. + +## Package and test the integration + +Keep provider code in your own import namespace and declare `openai-agents` plus any runtime client library as package dependencies. The example declares `openai-agents[docker]>=0.24.0,<0.25.0`. Treat that as the example's compatibility range, not a guarantee that every provider works across that range without testing. Do not publish into the `agents` namespace or require an SDK fork just to expose your client. + +Before distributing your provider: + +1. Build and install the distribution into a clean environment. Import it outside your source directory and run an application using `SandboxRunConfig(client=...)`. +2. Test options parsing and state serialization in a fresh interpreter after importing the package. Test unsupported and malformed inputs as well as a valid round trip. +3. Exercise the public `Runner` path with `ScriptedModel`: fresh client-owned sessions, application-owned live sessions, and persisted Runner resume. Verify resource cleanup after success, tool errors, timeout, cancellation, and partial startup. +4. Test workspace snapshot restore and supported file operations. Use framework mocks or the SDK's scripted test helpers for deterministic orchestration checks, while retaining real runtime tests for provider behavior. +5. Run real isolation, filesystem, network, and process tests on each supported runtime/platform combination. Mock tests cannot establish those properties. For a delegating adapter, reuse the underlying runtime contract and test that the adapter preserves it. + +The example README contains install and demo commands. The example intentionally leaves a new VM implementation, security policy, and runtime-specific compatibility promises to the provider author. diff --git a/docs/streaming.md b/docs/streaming.md index 84c3f548b7..ed98d33e99 100644 --- a/docs/streaming.md +++ b/docs/streaming.md @@ -68,6 +68,36 @@ If you are manually continuing from [`result.to_input_list(mode="normalized")`][ - If a streamed run stopped for tool approval, do not treat that as a new turn. Finish draining the stream, inspect `result.interruptions`, and resume from `result.to_state()` instead. - Use [`RunConfig.session_input_callback`][agents.run.RunConfig.session_input_callback] to customize how retrieved session history and the new user input are merged before the next model call. If you rewrite new-turn items there, the rewritten version is what gets persisted for that turn. +### Stop when your event consumer exits early + +The background run and the event consumer have separate lifetimes. If your application breaks out of the `async for` loop over events, raises an exception while handling an event, or cancels the consumer while it is handling an event, the run can continue making model and tool calls. Closing the event iterator with `aclose()` alone waits for the run to finish; it does not request cancellation. Cancellation while the consumer is awaiting the next event does request cancellation of the run, so do not rely on where cancellation happens to stop unwanted work. + +When one consumer owns the run, explicitly cancel on early exit and wait for cleanup. In this example, `agent` is your agent and `forward(event)` is your application's asynchronous event handler: + +```python +from agents import Runner + +result = Runner.run_streamed(agent, input="Please tell me 5 jokes.") +events = result.stream_events() +exhausted = False +try: + async for event in events: + await forward(event) + else: + exhausted = True +finally: + if not exhausted: + result.cancel() + await events.aclose() + # Use a fresh iterator in case cancellation already closed the original. + async for _event in result.stream_events(): + pass +``` + +Normal exhaustion leaves the result unchanged. An early `break`, a handler exception, or consumer cancellation enters the cleanup block. `cancel()` requests cancellation; consuming the cleanup stream runs the SDK's stream cleanup path. Neither `result.is_complete` nor finishing the cleanup stream guarantees that every asynchronous finalizer inside a tool has finished. Applications that depend on those finalizers must track and await tool cleanup separately. + +This example assumes one consumer and cooperative asynchronous tools. A further cancellation during cleanup can interrupt the cleanup block, and a cleanup failure can replace the original exception. Applications that must handle those cases need a supervised cleanup task and an explicit error-handling policy. Cancellation cannot undo tool effects that have already happened. + ## Run item events and agent events [`RunItemStreamEvent`][agents.stream_events.RunItemStreamEvent]s are higher level events. They inform you when an item has been fully generated. This allows you to push progress updates at the level of "message generated", "tool ran", etc, instead of each token. Similarly, [`AgentUpdatedStreamEvent`][agents.stream_events.AgentUpdatedStreamEvent] gives you updates when the current agent changes (e.g. as the result of a handoff). diff --git a/docs/tools.md b/docs/tools.md index 42c26c0ff7..755892d143 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -257,6 +257,8 @@ For commands or file edits that require review, set `needs_approval=True` on the To decide immediately in application code, set both `needs_approval` and `on_approval`. The SDK invokes `on_approval` only when the call requires approval and has no existing approval decision; setting the callback alone does not enable approval. See `examples/tools/shell.py` for a CLI prompt and `examples/tools/shell_human_in_the_loop.py` for manual interruption handling. The `examples/tools/apply_patch.py` example instead prompts inside its editor before changing files. +Persistent decisions for local `ShellTool` and `ApplyPatchTool` can carry across handoffs to another agent's tool of the same type and name, or across separate runs that reuse the same `RunContextWrapper`. Use per-call decisions when executor or editor permissions differ, and a fresh wrapper when starting a new run with different execution permissions; see the [approval scope warning](human_in_the_loop.md#how-the-approval-flow-works) for details, including why per-call decisions and context overrides during resume do not clear stored approvals. + Keep `needs_approval=False` when your application intentionally authorizes automatic execution, for example through an executor that enforces your sandbox policy. Hosted shell environments do not support the SDK's local `needs_approval` or `on_approval` settings. ### ComputerTool and the Responses computer tool diff --git a/examples/memory/custom_session.py b/examples/memory/custom_session.py new file mode 100644 index 0000000000..4e5c751034 --- /dev/null +++ b/examples/memory/custom_session.py @@ -0,0 +1,117 @@ +"""Adapt a synchronous history store using only public SDK interfaces. + +Run offline with: uv run python examples/memory/custom_session.py +The in-memory store stands in for a synchronous database client. A real client +must provide atomic batches and thread-safe operations, and set its own timeouts. +The adapter can live in a separate distribution that depends on openai-agents. +""" + +from __future__ import annotations + +import asyncio +from copy import deepcopy +from threading import Lock +from typing import Any + +from openai.types.responses import ResponseOutputMessage, ResponseOutputText + +from agents import Agent, RunConfig, Runner, SessionSettings, TResponseInputItem +from agents.memory import ( + await_session_mutation, + coerce_session_settings, + resolve_session_limit, +) +from agents.testing import ScriptedModel + + +class HistoryStore: + """Small synchronous demonstration store; data is lost when the process exits.""" + + def __init__(self) -> None: + self._items: dict[str, list[TResponseInputItem]] = {} + self._lock = Lock() + + def read(self, key: str, limit: int | None) -> list[TResponseInputItem]: + with self._lock: + items = self._items.get(key, []) + return deepcopy(items if limit is None else items[-limit:] if limit > 0 else []) + + def append(self, key: str, items: list[TResponseInputItem]) -> None: + batch = deepcopy(items) + with self._lock: + self._items.setdefault(key, []).extend(batch) + + def pop(self, key: str) -> TResponseInputItem | None: + with self._lock: + items = self._items.get(key, []) + return items.pop() if items else None + + def clear(self, key: str) -> None: + with self._lock: + self._items.pop(key, None) + + +class CustomSession: + """Session adapter for a caller-owned store; no SDK base class is required. + + This adapter does not close the store. The application owns its lifetime. + Individual store operations are atomic; overlapping runs for the same session + still require application-level coordination. + """ + + def __init__( + self, + session_id: str, + store: HistoryStore, + *, + session_settings: SessionSettings | dict[str, Any] | None = None, + ) -> None: + self.session_id = session_id + self.session_settings = ( + SessionSettings() + if session_settings is None + else coerce_session_settings(session_settings) + ) + self._store = store + + async def get_items(self, limit: int | None = None) -> list[TResponseInputItem]: + effective_limit = resolve_session_limit(limit, self.session_settings) + return await asyncio.to_thread(self._store.read, self.session_id, effective_limit) + + async def add_items(self, items: list[TResponseInputItem]) -> None: + await await_session_mutation(asyncio.to_thread(self._store.append, self.session_id, items)) + + async def pop_item(self) -> TResponseInputItem | None: + return await await_session_mutation(asyncio.to_thread(self._store.pop, self.session_id)) + + async def clear_session(self) -> None: + await await_session_mutation(asyncio.to_thread(self._store.clear, self.session_id)) + + +async def main() -> None: + store = HistoryStore() + session = CustomSession("conversation-1", store, session_settings={"limit": 10}) + model = ScriptedModel( + [ + [ + ResponseOutputMessage( + id=f"message-{index}", + role="assistant", + status="completed", + content=[ResponseOutputText(type="output_text", text=text, annotations=[])], + type="message", + ) + ] + for index, text in enumerate(("Hello!", "You said hello.")) + ] + ) + agent = Agent(name="Assistant", model=model) + config = RunConfig(tracing_disabled=True) + await Runner.run(agent, "Hello", session=session, run_config=config) + result = await Runner.run(agent, "What did I say?", session=session, run_config=config) + print(result.final_output) + print(f"Stored {len(await session.get_items())} history items.") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/realtime/twilio/requirements.txt b/examples/realtime/twilio/requirements.txt index 11d2808ced..7f07eb6fae 100644 --- a/examples/realtime/twilio/requirements.txt +++ b/examples/realtime/twilio/requirements.txt @@ -1,11 +1,13 @@ openai-agents -httpx2>=2.13.1 +httpx2>=2.13.1; python_version < "3.11" +httpx2>=2.15.1; python_version >= "3.11" pyjwt>=2.15.1 python-multipart>=0.0.32 urllib3>=2.8.0 fastapi -starlette>=1.7.0 +starlette>=1.7.0; python_version < "3.11" +starlette>=1.9.0; python_version >= "3.11" uvicorn[standard] websockets python-dotenv>=1.2.4 -twilio>=9.11.2,<10 +twilio>=9.12.0,<10 diff --git a/examples/realtime/twilio_sip/requirements.txt b/examples/realtime/twilio_sip/requirements.txt index 131cc1ab29..a5f15cba57 100644 --- a/examples/realtime/twilio_sip/requirements.txt +++ b/examples/realtime/twilio_sip/requirements.txt @@ -1,6 +1,8 @@ -fastapi>=0.142.2 -starlette>=1.7.0 -openai>=3.26.0,<4 -httpx2>=2.13.1 +fastapi>=0.150.0 +starlette>=1.7.0; python_version < "3.11" +starlette>=1.9.0; python_version >= "3.11" +openai>=3.35.1,<4 +httpx2>=2.13.1; python_version < "3.11" +httpx2>=2.15.1; python_version >= "3.11" uvicorn[standard]>=0.54.0 python-dotenv>=1.2.4 diff --git a/examples/run_examples.py b/examples/run_examples.py index 064288b3c6..1800459737 100644 --- a/examples/run_examples.py +++ b/examples/run_examples.py @@ -93,8 +93,6 @@ "examples/sandbox/extensions/daytona/usaspending_text2sql/setup_db.py", "examples/sandbox/extensions/e2b_runner.py", "examples/sandbox/extensions/modal_runner.py", - "examples/sandbox/extensions/runloop/capabilities.py", - "examples/sandbox/extensions/runloop/runner.py", # This local workflow gives model-selected shell commands access to the host. "examples/sandbox/extensions/temporal/local_hello_workflow.py", "examples/sandbox/extensions/temporal/temporal_sandbox_agent.py", diff --git a/examples/sandbox/extensions/README.md b/examples/sandbox/extensions/README.md index 3037543df4..6f94b0a998 100644 --- a/examples/sandbox/extensions/README.md +++ b/examples/sandbox/extensions/README.md @@ -6,10 +6,18 @@ They intentionally keep the flow simple: 1. Build a tiny manifest in memory. 2. Create a `SandboxAgent` that inspects that workspace through one shell tool. -3. Run the agent against E2B, Modal, Daytona, Cloudflare, Runloop, Blaxel, or Vercel. +3. Run the agent against E2B, Modal, Daytona, Cloudflare, Blaxel, or Vercel. All of these examples require `OPENAI_API_KEY`, because they call the model through the normal `Runner` path. Each cloud backend also needs its own provider credentials. +## Migrating from Runloop + +Runloop support has been removed from `openai-agents`. Imports from `agents.extensions.sandbox.runloop` and Runloop exports from `agents.extensions.sandbox`, including `RunloopSandboxClient`, no longer work. Applications that still need Runloop can stay on a previously released compatible version while migrating. + +The [Modal example](#modal) uses `ModalSandboxClient` and `ModalSandboxClientOptions` from `agents.extensions.sandbox`. Install `openai-agents[modal]`, configure Modal authentication, and set an app name. Replace the client and options in `SandboxRunConfig`, and adapt provider-specific mounts and platform operations. `ModalCloudBucketMountStrategy` provides native cloud bucket mounts; review and test its provider-specific behavior and credentials for your workload. + +Before switching, preserve any data you need from existing Runloop sandboxes. Runloop sandbox IDs, snapshots, and session state are provider-specific; do not assume Modal can resume them. Create a new Modal sandbox and restore the data through a supported mechanism. + ## E2B ### Setup @@ -228,84 +236,6 @@ export DAYTONA_API_KEY=... uv run python examples/sandbox/extensions/daytona/daytona_runner.py --stream ``` -## Runloop - -### Setup - -Install the repo extra: - -```bash -uv sync --extra runloop -``` - -Sign up for Runloop, no credit card required and $50 in credits @ [platform.runloop.ai](https://platform.runloop.ai/). -Export the required environment variables: - -```bash -export OPENAI_API_KEY=... -export RUNLOOP_API_KEY=... -``` - -### Run - -```bash -uv run python examples/sandbox/extensions/runloop/runner.py --stream -``` - -Useful flags: - -- `--blueprint-name ` -- `--pause-on-exit` -- `--root` - -Runloop-specific SDK features are also available directly on -`RunloopSandboxClientOptions` and `RunloopSandboxClient.platform`. Example: - -```python -from agents.extensions.sandbox.runloop import ( - RunloopAfterIdle, - RunloopGatewaySpec, - RunloopLaunchParameters, - RunloopMcpSpec, - RunloopSandboxClient, - RunloopSandboxClientOptions, - RunloopTunnelConfig, -) - -client = RunloopSandboxClient() -sandbox = await client.create( - options=RunloopSandboxClientOptions( - blueprint_name="python-3-12", - launch_parameters=RunloopLaunchParameters( - network_policy_id="np_123", - resource_size_request="MEDIUM", - after_idle=RunloopAfterIdle(idle_time_seconds=300, on_idle="suspend"), - ), - tunnel=RunloopTunnelConfig(auth_mode="authenticated"), - gateways={ - "OPENAI_GATEWAY": RunloopGatewaySpec( - gateway="openai", - secret="OPENAI_GATEWAY_SECRET", - ) - }, - mcp={ - "GITHUB_MCP": RunloopMcpSpec( - mcp_config="github-readonly", - secret="GITHUB_MCP_SECRET", - ) - }, - managed_secrets={"OPENAI_API_KEY": "..."}, - metadata={"team": "agents"}, - ) -) - -public_blueprints = await client.platform.blueprints.list_public() -public_benchmarks = await client.platform.benchmarks.list_public() -``` - -`managed_secrets` are stored as Runloop account secrets and only secret references are persisted in session state. An entry's value may instead be `RunloopExistingSecret()`, which attaches a secret that is already stored on the account by name, uploading nothing and leaving the stored value untouched. The platform facade also exposes Runloop-native helpers for blueprints, benchmarks, secrets, network policies, and axons. - -If you enable `--root`, Runloop launches the devbox with `launch_parameters.user_parameters={"username":"root","uid":0}`. In that mode, the default home and working directory become `/root`, so the example also uses `/root` as its manifest workspace root. If you configure root launch in your own code, either rely on that root-mode default or explicitly choose a `manifest.root` under `/root`. ## Blaxel ### Setup @@ -351,3 +281,7 @@ The runner also includes standalone demos for individual features. Pass - `drive` -- [Blaxel Drive mount](https://docs.blaxel.ai/Agent-drive/Overview) (persistent storage, requires `--drive-name`) Blaxel sandboxes support cloud bucket mounts (S3, R2, GCS) through `BlaxelCloudBucketMountStrategy` and persistent drive mounts through `BlaxelDriveMountStrategy`. See the [Blaxel Drive docs](https://docs.blaxel.ai/Agent-drive/Overview) for details. + +## Author an external provider + +The [external provider package](../external_provider/README.md) demonstrates a separately installable Docker-backed client and a deterministic Runner demo. It shows client injection and options registration without adding the package to the SDK. diff --git a/examples/sandbox/extensions/runloop/__init__.py b/examples/sandbox/extensions/runloop/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/examples/sandbox/extensions/runloop/capabilities.py b/examples/sandbox/extensions/runloop/capabilities.py deleted file mode 100644 index 17d1fff4a9..0000000000 --- a/examples/sandbox/extensions/runloop/capabilities.py +++ /dev/null @@ -1,1015 +0,0 @@ -from __future__ import annotations - -import argparse -import asyncio -import io -import json -import os -import sys -import time -import urllib.error -import urllib.request -import uuid -from pathlib import Path -from typing import Any, Literal, cast -from urllib.parse import urljoin - -from openai.types.responses import ResponseTextDeltaEvent -from pydantic import BaseModel - -from agents import ( - Agent, - ModelSettings, - Runner, -) -from agents.decorators import tool -from agents.run import RunConfig -from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig - -if __package__ is None or __package__ == "": - sys.path.insert(0, str(Path(__file__).resolve().parents[4])) - -from examples.sandbox.misc.example_support import text_manifest, tool_call_name -from examples.sandbox.misc.workspace_shell import WorkspaceShellCapability - -try: - from agents.extensions.sandbox import ( - DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT, - DEFAULT_RUNLOOP_WORKSPACE_ROOT, - RunloopAfterIdle, - RunloopGatewaySpec, - RunloopLaunchParameters, - RunloopMcpSpec, - RunloopSandboxClient, - RunloopSandboxClientOptions, - RunloopSandboxSessionState, - RunloopTunnelConfig, - RunloopUserParameters, - ) -except Exception as exc: # pragma: no cover - import path depends on optional extras - raise SystemExit( - "Runloop sandbox examples require the optional repo extra.\n" - "Install it with: uv sync --extra runloop" - ) from exc - - -DEFAULT_MODEL = "gpt-5.6-sol" -DEFAULT_HTTP_PORT = 8123 -DEFAULT_AGENT_PROMPT = ( - "Inspect this Runloop sandbox workspace, verify the configuration using the shell tool, " - "and summarize which Runloop-specific capabilities were exercised." -) -EXAMPLE_RESOURCE_SLUG = "runloop-capabilities-example" -PERSISTENT_SECRET_NAME = "RUNLOOP_CAPABILITIES_EXAMPLE_TOKEN" -PERSISTENT_SECRET_VALUE = "runloop-capabilities-example-token" -PERSISTENT_NETWORK_POLICY_NAME = "runloop-capabilities-example-policy" -HTTP_LOG_PATH = Path(".runloop-http.log") -RUNTIME_CONTEXT_PATH = Path("runtime_context.json") -AGENT_PROOF_PATH = Path("verification/agent-proof.txt") - - -class RunloopResourceQueryResult(BaseModel): - resource_type: Literal["secret", "network_policy"] - name: str - found: bool - id: str | None = None - description: str | None = None - - -class RunloopResourceBootstrapResult(BaseModel): - resource_type: Literal["secret", "network_policy"] - name: str - action: Literal["created", "reused", "override"] - id: str | None = None - found_before_bootstrap: bool - - -def _phase(title: str) -> None: - print(f"\n=== {title} ===", flush=True) - - -def _require_env(name: str) -> None: - if os.environ.get(name): - return - raise SystemExit(f"{name} must be set before running this example.") - - -def _run_id() -> str: - return uuid.uuid4().hex[:8] - - -def _summarize_resource(item: object, fields: tuple[str, ...]) -> dict[str, object]: - summary: dict[str, object] = {} - for field in fields: - value = getattr(item, field, None) - if value is not None: - summary[field] = value - return summary - - -async def _collect_async_items(items: Any, *, limit: int) -> list[Any]: - collected: list[Any] = [] - async for item in items: - collected.append(item) - if len(collected) >= limit: - break - return collected - - -def _status_code(exc: BaseException) -> int | None: - status_code = getattr(exc, "status_code", None) - if isinstance(status_code, int): - return status_code - response = getattr(exc, "response", None) - response_status = getattr(response, "status_code", None) - return response_status if isinstance(response_status, int) else None - - -def _is_not_found(exc: BaseException) -> bool: - return _status_code(exc) == 404 - - -def _error_message(exc: BaseException) -> str | None: - message = getattr(exc, "message", None) - if isinstance(message, str): - return message - body = getattr(exc, "body", None) - if isinstance(body, dict): - body_message = body.get("message") - if isinstance(body_message, str): - return body_message - return None - - -def _is_conflict(exc: BaseException) -> bool: - status_code = _status_code(exc) - if status_code == 409: - return True - if status_code == 400: - message = _error_message(exc) - return isinstance(message, str) and "already exists" in message.lower() - return False - - -async def _collect_maybe_async_items(items: Any, *, limit: int) -> list[Any]: - if hasattr(items, "__aiter__"): - return await _collect_async_items(items, limit=limit) - return list(items)[:limit] - - -async def _read_text(session: Any, path: Path) -> str: - data = await session.read(path) - try: - payload = data.read() - finally: - data.close() - if isinstance(payload, bytes): - return payload.decode("utf-8") - return str(payload) - - -async def _write_json(session: Any, path: Path, payload: dict[str, object]) -> None: - await session.write( - path, io.BytesIO(json.dumps(payload, indent=2, sort_keys=True).encode("utf-8")) - ) - - -def _build_manifest(*, workspace_root: str, context: dict[str, object]) -> Manifest: - manifest = text_manifest( - { - "README.md": ( - "# Runloop Capabilities Example\n\n" - "This workspace is used to validate the Runloop-specific sandbox integration end " - "to end.\n" - ), - "checklist.md": ( - "# Checklist\n\n" - "1. Inspect the workspace.\n" - "2. Verify the resource discovery results in the context files.\n" - "3. Confirm the managed secret is available without printing its full value.\n" - "4. Confirm the HTTP preview server and verification file.\n" - "5. Summarize what Runloop-native features were exercised and whether persistent " - "resources were reused or created.\n" - ), - "platform_context.json": json.dumps(context, indent=2, sort_keys=True) + "\n", - } - ) - return Manifest(root=workspace_root, entries=manifest.entries) - - -def _build_sandbox_agent( - *, model: str, manifest: Manifest, managed_secret_name: str -) -> SandboxAgent: - return SandboxAgent( - name="Runloop Capabilities Guide", - model=model, - instructions=( - "Inspect the Runloop sandbox workspace carefully before answering. Use the shell tool " - "to verify what happened in the environment and keep the final response concise. " - "Follow this sequence:\n" - "1. Run `pwd` and `find . -maxdepth 3 -type f | sort`.\n" - "2. Read `README.md`, `checklist.md`, `platform_context.json`, and `runtime_context.json`.\n" - "3. Report whether the managed secret and network policy existed before bootstrap by " - "reading the query/bootstrap summaries from the context files.\n" - f"4. Confirm whether `${managed_secret_name}` is set, but never print the full value. " - "Only report whether it exists and its character length.\n" - f"5. Read `{HTTP_LOG_PATH.as_posix()}` and confirm the HTTP server started.\n" - f"6. Create `{AGENT_PROOF_PATH.as_posix()}` with these exact lines:\n" - " runloop_capabilities_verified=true\n" - " managed_secret_checked=true\n" - " tunnel_verified=true\n" - "7. Print that verification file from the shell.\n" - "8. Final answer: 2 short sentences naming the specific Runloop features exercised, " - "including whether the persistent secret and policy were reused or created.\n" - "Only mention facts you verified from files, environment inspection, or shell output." - ), - default_manifest=manifest, - capabilities=[WorkspaceShellCapability()], - model_settings=ModelSettings(tool_choice="required"), - ) - - -def _build_query_agent( - *, - model: str, - query_secret_tool: Any, - query_policy_tool: Any, - managed_secret_name: str, - network_policy_name: str, -) -> Agent: - return Agent( - name="Runloop Resource Discovery Guide", - model=model, - instructions=( - "Use the provided Runloop query tools to check whether the persistent example " - "resources already exist before any create step. Keep the final answer concise." - ), - tools=[query_secret_tool, query_policy_tool], - model_settings=ModelSettings(tool_choice="required"), - ).clone( - instructions=( - "Use the provided Runloop query tools to check whether the persistent example " - "resources already exist before any create step. Keep the final answer concise." - ), - handoff_description=None, - output_type=None, - ) - - -def _stream_event_banner(event_name: str) -> str | None: - if event_name == "tool_called": - return "[tool call]" - if event_name == "tool_output": - return "[tool output]" - return None - - -def _runloop_state(session: Any) -> RunloopSandboxSessionState: - return cast(RunloopSandboxSessionState, session.state) - - -async def _run_plain_agent( - *, - agent: Agent, - prompt: str, - workflow_name: str, - stream: bool, -) -> str: - if not stream: - result = await Runner.run(agent, prompt, run_config=RunConfig(workflow_name=workflow_name)) - print(result.final_output) - return str(result.final_output) - - stream_result = Runner.run_streamed( - agent, - prompt, - run_config=RunConfig(workflow_name=workflow_name), - ) - saw_text_delta = False - saw_any_text = False - - async for event in stream_result.stream_events(): - if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent): - if not saw_text_delta: - print("assistant> ", end="", flush=True) - saw_text_delta = True - print(event.data.delta, end="", flush=True) - saw_any_text = True - continue - - if event.type != "run_item_stream_event": - continue - - banner = _stream_event_banner(event.name) - if banner is None: - continue - if saw_text_delta: - print() - saw_text_delta = False - print(f"{banner}: {tool_call_name(event.item.raw_item) or 'tool'}", flush=True) - - if saw_text_delta: - print() - if not saw_any_text: - print(stream_result.final_output) - return str(stream_result.final_output) - - -async def _run_sandbox_agent( - *, - agent: SandboxAgent, - prompt: str, - session: Any, - workflow_name: str, - stream: bool, -) -> str: - if not stream: - result = await Runner.run( - agent, - prompt, - run_config=RunConfig( - sandbox=SandboxRunConfig(session=session), - workflow_name=workflow_name, - ), - ) - print(result.final_output) - return str(result.final_output) - - stream_result = Runner.run_streamed( - agent, - prompt, - run_config=RunConfig( - sandbox=SandboxRunConfig(session=session), - workflow_name=workflow_name, - ), - ) - saw_text_delta = False - saw_any_text = False - - async for event in stream_result.stream_events(): - if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent): - if not saw_text_delta: - print("assistant> ", end="", flush=True) - saw_text_delta = True - print(event.data.delta, end="", flush=True) - saw_any_text = True - continue - - if event.type != "run_item_stream_event": - continue - - banner = _stream_event_banner(event.name) - if banner is None: - continue - if saw_text_delta: - print() - saw_text_delta = False - print(f"{banner}: {tool_call_name(event.item.raw_item) or 'tool'}", flush=True) - - if saw_text_delta: - print() - if not saw_any_text: - print(stream_result.final_output) - return str(stream_result.final_output) - - -async def _start_http_server(session: Any, *, port: int, workspace_root: str) -> None: - command = ( - "python -m http.server " - f"{port} --bind 0.0.0.0 --directory {workspace_root} " - f"> {HTTP_LOG_PATH.as_posix()} 2>&1 &" - ) - result = await session.exec(command, shell=True, timeout=10) - if not result.ok(): - raise RuntimeError(result.stderr.decode("utf-8", errors="replace")) - - -def _build_endpoint_url(endpoint: Any) -> str: - scheme = "https" if endpoint.tls else "http" - port = endpoint.port - host = endpoint.host - if (scheme == "https" and port == 443) or (scheme == "http" and port == 80): - return f"{scheme}://{host}/" - return f"{scheme}://{host}:{port}/" - - -async def _fetch_text(url: str, *, timeout_s: float) -> str: - def _fetch() -> str: - with urllib.request.urlopen(url, timeout=timeout_s) as response: - payload = response.read() - if isinstance(payload, bytes): - return payload.decode("utf-8", errors="replace") - return str(payload) - - return await asyncio.to_thread(_fetch) - - -async def _poll_http_preview(url: str, *, expected_substring: str, timeout_s: float) -> str: - deadline = time.monotonic() + timeout_s - last_error: Exception | None = None - while time.monotonic() < deadline: - try: - body = await _fetch_text(url, timeout_s=5.0) - if expected_substring in body: - return body - except (urllib.error.URLError, TimeoutError) as exc: - last_error = exc - await asyncio.sleep(2) - if last_error is not None: - raise RuntimeError(f"HTTP preview never became ready: {last_error}") from last_error - raise RuntimeError("HTTP preview never returned the expected content.") - - -async def _preflight_public_resources(client: RunloopSandboxClient) -> dict[str, object]: - blueprints = await _collect_async_items( - await client.platform.blueprints.list_public(limit=3), - limit=3, - ) - benchmarks = await _collect_async_items( - await client.platform.benchmarks.list_public(limit=3), - limit=3, - ) - - blueprint_summaries = [ - _summarize_resource(item, ("id", "name", "status")) for item in blueprints - ] - benchmark_summaries = [ - _summarize_resource(item, ("id", "name", "description")) for item in benchmarks - ] - - if blueprint_summaries: - print("public blueprints:") - for summary in blueprint_summaries: - print(f" - {summary}") - else: - print("public blueprints: none returned") - - if benchmark_summaries: - print("public benchmarks:") - for summary in benchmark_summaries: - print(f" - {summary}") - else: - print("public benchmarks: none returned") - - return { - "public_blueprints": blueprint_summaries, - "public_benchmarks": benchmark_summaries, - } - - -async def _query_runloop_secret( - client: RunloopSandboxClient, - *, - name: str, -) -> RunloopResourceQueryResult: - try: - secret = cast(Any, await client.platform.secrets.get(name)) - except Exception as exc: - if _is_not_found(exc): - return RunloopResourceQueryResult(resource_type="secret", name=name, found=False) - raise - - return RunloopResourceQueryResult( - resource_type="secret", - name=name, - found=True, - id=cast(str | None, getattr(secret, "id", None)), - ) - - -async def _query_runloop_network_policy( - client: RunloopSandboxClient, - *, - name: str, -) -> RunloopResourceQueryResult: - policies = await _collect_maybe_async_items( - await client.platform.network_policies.list(name=name, limit=10), - limit=10, - ) - for policy in policies: - policy_id = cast(str | None, getattr(policy, "id", None)) - if policy_id is None: - continue - info = cast(Any, await client.platform.network_policies.get(policy_id).get_info()) - if getattr(info, "name", None) != name: - continue - return RunloopResourceQueryResult( - resource_type="network_policy", - name=name, - found=True, - id=cast(str | None, getattr(info, "id", None)) or policy_id, - description=cast(str | None, getattr(info, "description", None)), - ) - - return RunloopResourceQueryResult(resource_type="network_policy", name=name, found=False) - - -def _build_resource_query_tools( - client: RunloopSandboxClient, - *, - managed_secret_name: str, - network_policy_name: str, -) -> tuple[list[Any], dict[str, RunloopResourceQueryResult]]: - query_results: dict[str, RunloopResourceQueryResult] = {} - - @tool - async def query_runloop_secret(name: str) -> RunloopResourceQueryResult: - """Query whether a Runloop secret exists by name and return non-sensitive metadata.""" - - result = await _query_runloop_secret(client, name=name) - query_results["secret"] = result - return result - - @tool - async def query_runloop_network_policy(name: str) -> RunloopResourceQueryResult: - """Query whether a Runloop network policy exists by name and return basic metadata.""" - - result = await _query_runloop_network_policy(client, name=name) - query_results["network_policy"] = result - return result - - tools = [query_runloop_secret, query_runloop_network_policy] - _ = (managed_secret_name, network_policy_name) - return tools, query_results - - -async def _run_resource_query_phase( - client: RunloopSandboxClient, - *, - model: str, - stream: bool, - managed_secret_name: str, - network_policy_name: str, -) -> tuple[dict[str, RunloopResourceQueryResult], str]: - tools, query_results = _build_resource_query_tools( - client, - managed_secret_name=managed_secret_name, - network_policy_name=network_policy_name, - ) - query_agent = Agent( - name="Runloop Resource Discovery Guide", - model=model, - instructions=( - "Use both query tools before answering. You are checking whether the persistent " - "Runloop example resources already exist before any create step.\n\n" - f"1. Call `query_runloop_secret` with `{managed_secret_name}`.\n" - f"2. Call `query_runloop_network_policy` with `{network_policy_name}`.\n" - "3. Final answer in 2 short sentences stating whether each resource already exists." - ), - tools=tools, - model_settings=ModelSettings(tool_choice="required"), - ) - prompt = ( - "Check whether the persistent Runloop secret and network policy for this example already " - "exist before the script attempts any create or reuse step." - ) - output = await _run_plain_agent( - agent=query_agent, - prompt=prompt, - workflow_name="Runloop resource query example", - stream=stream, - ) - if "secret" not in query_results or "network_policy" not in query_results: - raise RuntimeError("The query agent did not call both Runloop resource query tools.") - return query_results, output - - -async def _bootstrap_persistent_resources( - client: RunloopSandboxClient, - *, - managed_secret_name: str, - managed_secret_value: str, - network_policy_name: str, - network_policy_id_override: str | None, - query_results: dict[str, RunloopResourceQueryResult], - axon_name: str | None, -) -> dict[str, object]: - secret_query = query_results["secret"] - policy_query = query_results["network_policy"] - - bootstrap: dict[str, object] = { - "managed_secret_value": managed_secret_value, - "secret": RunloopResourceBootstrapResult( - resource_type="secret", - name=managed_secret_name, - action="reused" if secret_query.found else "created", - id=secret_query.id, - found_before_bootstrap=secret_query.found, - ), - "network_policy": RunloopResourceBootstrapResult( - resource_type="network_policy", - name=network_policy_name, - action="override" - if network_policy_id_override - else ("reused" if policy_query.found else "created"), - id=network_policy_id_override or policy_query.id, - found_before_bootstrap=policy_query.found, - ), - "axon_id": None, - "axon_name": axon_name, - } - - secret_result = cast(RunloopResourceBootstrapResult, bootstrap["secret"]) - if not secret_query.found: - created_secret = cast( - Any, - await client.platform.secrets.create( - name=managed_secret_name, value=managed_secret_value - ), - ) - secret_result.id = cast(str | None, getattr(created_secret, "id", None)) - print( - "persistent secret bootstrap:", - secret_result.model_dump(mode="json"), - ) - - policy_result = cast(RunloopResourceBootstrapResult, bootstrap["network_policy"]) - if network_policy_id_override is None and not policy_query.found: - try: - created_policy = cast( - Any, - await client.platform.network_policies.create( - name=network_policy_name, - allow_all=True, - description="Persistent network policy for the Runloop capabilities example.", - ), - ) - except Exception as exc: - if not _is_conflict(exc): - raise - policy_result.action = "reused" - policy_result.found_before_bootstrap = True - refreshed_policy = await _query_runloop_network_policy(client, name=network_policy_name) - if not refreshed_policy.found or refreshed_policy.id is None: - raise RuntimeError( - "Runloop reported a network policy conflict, but the existing policy " - f"{network_policy_name!r} could not be resolved." - ) from exc - policy_result.id = refreshed_policy.id - else: - policy_result.id = cast(str | None, getattr(created_policy, "id", None)) - if policy_result.id is None: - refreshed_policy = await _query_runloop_network_policy( - client, name=network_policy_name - ) - policy_result.id = refreshed_policy.id - if policy_result.id is None: - raise RuntimeError( - f"Runloop network policy {network_policy_name!r} did not resolve to an ID." - ) - print( - "persistent network policy bootstrap:", - policy_result.model_dump(mode="json"), - ) - - if axon_name is not None: - axon = cast(Any, await client.platform.axons.create(name=axon_name)) - await client.platform.axons.query_sql( - cast(str, axon.id), - sql="CREATE TABLE IF NOT EXISTS events (id INTEGER PRIMARY KEY AUTOINCREMENT, kind TEXT NOT NULL)", - ) - await client.platform.axons.batch_sql( - cast(str, axon.id), - statements=[ - {"sql": "INSERT INTO events (kind) VALUES (?)", "params": ["capabilities"]}, - {"sql": "INSERT INTO events (kind) VALUES (?)", "params": ["agent_guided"]}, - ], - ) - query_result = cast( - Any, - await client.platform.axons.query_sql( - cast(str, axon.id), - sql="SELECT COUNT(*) AS total_events FROM events", - ), - ) - publish_result = cast( - Any, - await client.platform.axons.publish( - cast(str, axon.id), - event_type="capabilities_example", - origin="AGENT_EVENT", - payload=json.dumps({"axon_name": axon_name}), - source="openai-agents-python", - ), - ) - bootstrap["axon_id"] = cast(str, axon.id) - print( - "axon demo created:", - { - "id": cast(str, axon.id), - "name": axon_name, - "rows": query_result.rows, - "published": getattr(publish_result, "published", None), - }, - ) - - return bootstrap - - -def _optional_gateways(args: argparse.Namespace) -> dict[str, RunloopGatewaySpec]: - if not (args.gateway_env_var and args.gateway_name and args.gateway_secret_name): - return {} - return { - args.gateway_env_var: RunloopGatewaySpec( - gateway=args.gateway_name, - secret=args.gateway_secret_name, - ) - } - - -def _optional_mcp(args: argparse.Namespace) -> dict[str, RunloopMcpSpec]: - if not (args.mcp_env_var and args.mcp_config and args.mcp_secret_name): - return {} - return { - args.mcp_env_var: RunloopMcpSpec( - mcp_config=args.mcp_config, - secret=args.mcp_secret_name, - ) - } - - -async def main(args: argparse.Namespace) -> None: - _require_env("OPENAI_API_KEY") - _require_env("RUNLOOP_API_KEY") - - workspace_root = ( - DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT if args.root else DEFAULT_RUNLOOP_WORKSPACE_ROOT - ) - run_id = _run_id() - metadata = { - "example": "runloop-capabilities", - "run_id": run_id, - } - - client = RunloopSandboxClient() - session = None - resumed = None - session_closed = False - resumed_closed = False - - try: - _phase("Public Resource Discovery") - public_context = await _preflight_public_resources(client) - - _phase("Agent Resource Discovery") - query_results, query_agent_output = await _run_resource_query_phase( - client, - model=args.model, - stream=args.stream, - managed_secret_name=PERSISTENT_SECRET_NAME, - network_policy_name=PERSISTENT_NETWORK_POLICY_NAME, - ) - print( - "resource query results:", - {key: value.model_dump(mode="json") for key, value in query_results.items()}, - ) - - _phase("Persistent Resource Bootstrap") - axon_name = f"{EXAMPLE_RESOURCE_SLUG}-axon-{run_id}" if args.with_axon_demo else None - bootstrap = await _bootstrap_persistent_resources( - client, - managed_secret_name=PERSISTENT_SECRET_NAME, - managed_secret_value=PERSISTENT_SECRET_VALUE, - network_policy_name=PERSISTENT_NETWORK_POLICY_NAME, - network_policy_id_override=args.network_policy_id, - query_results=query_results, - axon_name=axon_name, - ) - secret_bootstrap = cast(RunloopResourceBootstrapResult, bootstrap["secret"]) - network_policy_bootstrap = cast(RunloopResourceBootstrapResult, bootstrap["network_policy"]) - network_policy_id = network_policy_bootstrap.id - - context = { - "example_slug": EXAMPLE_RESOURCE_SLUG, - "workspace_root": workspace_root, - "requested_blueprint_name": args.blueprint_name, - "public_resources": public_context, - "resource_query_agent_output": query_agent_output, - "resource_queries": { - key: value.model_dump(mode="json") for key, value in query_results.items() - }, - "resource_bootstrap": { - "secret": secret_bootstrap.model_dump(mode="json"), - "network_policy": network_policy_bootstrap.model_dump(mode="json"), - "axon_id": bootstrap["axon_id"], - "axon_name": bootstrap["axon_name"], - }, - "managed_secret_env_var": PERSISTENT_SECRET_NAME, - "network_policy_id": network_policy_id, - "metadata": metadata, - "gateway_bindings": sorted(_optional_gateways(args)), - "mcp_bindings": sorted(_optional_mcp(args)), - } - - manifest = _build_manifest(workspace_root=workspace_root, context=context) - agent = _build_sandbox_agent( - model=args.model, - manifest=manifest, - managed_secret_name=PERSISTENT_SECRET_NAME, - ) - options = RunloopSandboxClientOptions( - blueprint_name=args.blueprint_name, - pause_on_exit=True, - exposed_ports=(args.http_port,), - user_parameters=(RunloopUserParameters(username="root", uid=0) if args.root else None), - launch_parameters=RunloopLaunchParameters( - network_policy_id=network_policy_id, - resource_size_request=args.resource_size, - after_idle=RunloopAfterIdle(idle_time_seconds=300, on_idle="suspend"), - launch_commands=["echo runloop-capabilities-example"], - ), - tunnel=RunloopTunnelConfig( - auth_mode="open", - http_keep_alive=True, - wake_on_http=True, - ), - gateways=_optional_gateways(args), - mcp=_optional_mcp(args), - metadata=metadata, - managed_secrets={PERSISTENT_SECRET_NAME: PERSISTENT_SECRET_VALUE}, - ) - - _phase("Sandbox Create") - session = await client.create(manifest=manifest, options=options) - await session.start() - session_state = _runloop_state(session) - print( - "session started:", - { - "devbox_id": session_state.devbox_id, - "secret_refs": session_state.secret_refs, - "metadata": session_state.metadata, - }, - ) - - _phase("Tunnel Check") - await _write_json( - session, - RUNTIME_CONTEXT_PATH, - { - **context, - "devbox_id": session_state.devbox_id, - "secret_refs": session_state.secret_refs, - "runtime_phase": "before_tunnel_check", - }, - ) - await _start_http_server(session, port=args.http_port, workspace_root=workspace_root) - endpoint = await session.resolve_exposed_port(args.http_port) - preview_url = urljoin(_build_endpoint_url(endpoint), "README.md") - preview_body = await _poll_http_preview( - preview_url, - expected_substring="Runloop Capabilities Example", - timeout_s=45.0, - ) - print("resolved tunnel:", preview_url) - await _write_json( - session, - RUNTIME_CONTEXT_PATH, - { - **context, - "devbox_id": session_state.devbox_id, - "secret_refs": session_state.secret_refs, - "tunnel_url": preview_url, - "http_preview_contains_readme": "Runloop Capabilities Example" in preview_body, - "runtime_phase": "before_agent_run", - }, - ) - - _phase("Agent Verification") - await _run_sandbox_agent( - agent=agent, - prompt=args.prompt, - session=session, - workflow_name="Runloop capabilities example", - stream=args.stream, - ) - proof_text = await _read_text(session, AGENT_PROOF_PATH) - print("agent proof:") - print(proof_text.rstrip()) - - _phase("Suspend") - await session.aclose() - session_closed = True - print("session persisted and suspended") - - _phase("Resume Check") - resumed = await client.resume(session.state) - await resumed.start() - resumed_state = _runloop_state(resumed) - resumed_runtime_context = await _read_text(resumed, RUNTIME_CONTEXT_PATH) - resumed_proof_text = await _read_text(resumed, AGENT_PROOF_PATH) - print("resumed runtime context bytes:", len(resumed_runtime_context.encode("utf-8"))) - print("resumed proof:") - print(resumed_proof_text.rstrip()) - resumed_state.pause_on_exit = False - await resumed.aclose() - resumed_closed = True - print("resumed session cleaned up with delete semantics") - - _phase("Persistent Resource Summary") - print( - "persistent resources retained:", - { - "secret": secret_bootstrap.model_dump(mode="json"), - "network_policy": network_policy_bootstrap.model_dump(mode="json"), - }, - ) - if bootstrap["axon_id"] is not None: - print( - "axon retained for manual cleanup:", - { - "axon_id": bootstrap["axon_id"], - "axon_name": bootstrap["axon_name"], - }, - ) - finally: - if resumed is not None and not resumed_closed: - try: - _runloop_state(resumed).pause_on_exit = False - await resumed.aclose() - except Exception as exc: - print(f"warning: failed to close resumed session cleanly: {exc}") - elif session is not None and not session_closed: - try: - _runloop_state(session).pause_on_exit = False - await session.aclose() - except Exception as exc: - print(f"warning: failed to close initial session cleanly: {exc}") - elif session is not None and session_closed and resumed is None: - try: - cleanup_session = await client.resume(session.state) - _runloop_state(cleanup_session).pause_on_exit = False - await cleanup_session.aclose() - except Exception as exc: - print(f"warning: failed to resume suspended session for cleanup: {exc}") - - await client.close() - - -def _build_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser() - parser.add_argument("--model", default=DEFAULT_MODEL, help="Model name to use.") - parser.add_argument( - "--prompt", default=DEFAULT_AGENT_PROMPT, help="Prompt to send to the agent." - ) - parser.add_argument("--blueprint-name", default=None, help="Optional Runloop blueprint name.") - parser.add_argument( - "--resource-size", - default="MEDIUM", - choices=["X_SMALL", "SMALL", "MEDIUM", "LARGE", "X_LARGE", "XX_LARGE", "CUSTOM_SIZE"], - help="Runloop resource size request for the devbox.", - ) - parser.add_argument( - "--network-policy-id", - default=None, - help="Optional Runloop network policy id override. Without this flag, the example reuses or creates the persistent example policy by name.", - ) - parser.add_argument( - "--http-port", - type=int, - default=DEFAULT_HTTP_PORT, - help="Port used by the preview HTTP server.", - ) - parser.add_argument( - "--root", - action="store_true", - default=False, - help="Launch the Runloop devbox as root. The workspace root becomes /root.", - ) - parser.add_argument( - "--stream", - action="store_true", - default=False, - help="Stream the agent response and tool activity.", - ) - parser.add_argument( - "--with-axon-demo", - action="store_true", - default=False, - help="Also create and use a temporary Axon. This leaves the Axon behind for manual cleanup.", - ) - parser.add_argument( - "--gateway-env-var", default=None, help="Env var name for a gateway binding." - ) - parser.add_argument( - "--gateway-name", default=None, help="Runloop gateway name for the binding." - ) - parser.add_argument( - "--gateway-secret-name", - default=None, - help="Runloop secret name used by the gateway binding.", - ) - parser.add_argument("--mcp-env-var", default=None, help="Env var name for an MCP binding.") - parser.add_argument( - "--mcp-config", default=None, help="Runloop MCP config name for the binding." - ) - parser.add_argument( - "--mcp-secret-name", - default=None, - help="Runloop secret name used by the MCP binding.", - ) - return parser - - -if __name__ == "__main__": - asyncio.run(main(_build_parser().parse_args())) diff --git a/examples/sandbox/extensions/runloop/runner.py b/examples/sandbox/extensions/runloop/runner.py deleted file mode 100644 index 9f7438d608..0000000000 --- a/examples/sandbox/extensions/runloop/runner.py +++ /dev/null @@ -1,172 +0,0 @@ -""" -Minimal Runloop-backed sandbox example for manual validation. - -This mirrors the other cloud extension examples: it creates a tiny workspace, asks a sandboxed -agent to inspect it through one shell tool, and prints a short answer. -""" - -import argparse -import asyncio -import os -import sys -from pathlib import Path - -from openai.types.responses import ResponseTextDeltaEvent - -from agents import ModelSettings, Runner -from agents.run import RunConfig -from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig -from agents.sandbox.snapshot import NoopSnapshotSpec - -if __package__ is None or __package__ == "": - sys.path.insert(0, str(Path(__file__).resolve().parents[4])) - -from examples.sandbox.misc.example_support import text_manifest -from examples.sandbox.misc.workspace_shell import WorkspaceShellCapability - -try: - from agents.extensions.sandbox import ( - DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT, - DEFAULT_RUNLOOP_WORKSPACE_ROOT, - RunloopSandboxClient, - RunloopSandboxClientOptions, - RunloopUserParameters, - ) -except Exception as exc: # pragma: no cover - import path depends on optional extras - raise SystemExit( - "Runloop sandbox examples require the optional repo extra.\n" - "Install it with: uv sync --extra runloop" - ) from exc - - -DEFAULT_QUESTION = "Summarize this cloud sandbox workspace in 2 sentences." - - -def _build_manifest(*, workspace_root: str) -> Manifest: - manifest = text_manifest( - { - "README.md": ( - "# Runloop Demo Workspace\n\n" - "This workspace exists to validate the Runloop sandbox backend manually.\n" - ), - "launch.md": ( - "# Launch\n\n" - "- Customer: Contoso Logistics.\n" - "- Goal: validate the remote sandbox agent path.\n" - "- Current status: Runloop backend smoke and app-server connectivity are passing.\n" - ), - "tasks.md": ( - "# Tasks\n\n" - "1. Inspect the workspace files.\n" - "2. Summarize the setup and any notable status in two sentences.\n" - ), - } - ) - return Manifest(root=workspace_root, entries=manifest.entries) - - -def _require_env(name: str) -> None: - if os.environ.get(name): - return - raise SystemExit(f"{name} must be set before running this example.") - - -async def main( - *, - model: str, - question: str, - pause_on_exit: bool, - blueprint_name: str | None, - root: bool, - stream: bool, -) -> None: - _require_env("OPENAI_API_KEY") - _require_env("RUNLOOP_API_KEY") - - workspace_root = DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT if root else DEFAULT_RUNLOOP_WORKSPACE_ROOT - manifest = _build_manifest(workspace_root=workspace_root) - agent = SandboxAgent( - name="Runloop Sandbox Assistant", - model=model, - instructions=( - "Answer questions about the sandbox workspace. Inspect the files before answering " - "and keep the response concise. " - "Do not invent files or statuses that are not present in the workspace. Cite the " - "file names you inspected." - ), - default_manifest=manifest, - capabilities=[WorkspaceShellCapability()], - model_settings=ModelSettings(tool_choice="required"), - ) - - client = RunloopSandboxClient() - run_config = RunConfig( - sandbox=SandboxRunConfig( - client=client, - options=RunloopSandboxClientOptions( - blueprint_name=blueprint_name, - pause_on_exit=pause_on_exit, - user_parameters=(RunloopUserParameters(username="root", uid=0) if root else None), - ), - snapshot=NoopSnapshotSpec(), - ), - workflow_name="Runloop sandbox example", - ) - - try: - if not stream: - result = await Runner.run(agent, question, run_config=run_config) - print(result.final_output) - return - - stream_result = Runner.run_streamed(agent, question, run_config=run_config) - saw_text_delta = False - async for event in stream_result.stream_events(): - if event.type == "raw_response_event" and isinstance( - event.data, ResponseTextDeltaEvent - ): - if not saw_text_delta: - print("assistant> ", end="", flush=True) - saw_text_delta = True - print(event.data.delta, end="", flush=True) - - if saw_text_delta: - print() - finally: - await client.close() - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--model", default="gpt-5.6-sol", help="Model name to use.") - parser.add_argument("--question", default=DEFAULT_QUESTION, help="Prompt to send to the agent.") - parser.add_argument( - "--pause-on-exit", - action="store_true", - default=False, - help="Suspend the Runloop devbox on shutdown instead of deleting it.", - ) - parser.add_argument( - "--blueprint-name", - default=None, - help="Optional Runloop blueprint name to use when creating the devbox.", - ) - parser.add_argument( - "--root", - action="store_true", - default=False, - help="Launch the Runloop devbox as root. The default home/workspace root becomes /root.", - ) - parser.add_argument("--stream", action="store_true", default=False, help="Stream the response.") - args = parser.parse_args() - - asyncio.run( - main( - model=args.model, - question=args.question, - pause_on_exit=args.pause_on_exit, - blueprint_name=args.blueprint_name, - root=args.root, - stream=args.stream, - ) - ) diff --git a/examples/sandbox/external_provider/README.md b/examples/sandbox/external_provider/README.md new file mode 100644 index 0000000000..e6115b26a7 --- /dev/null +++ b/examples/sandbox/external_provider/README.md @@ -0,0 +1,36 @@ +# External sandbox provider package + +This directory is a separately installable Python distribution. It demonstrates importing a client from outside `agents`, registering package-specific options through subclass creation, and passing the client to `SandboxRunConfig`. It has no entry-point registration and does not patch the SDK's import namespace. + +`ExampleDockerClient` is a concrete adapter over the SDK's Docker backend, not an independently implemented container runtime. It translates its `ExampleDockerOptions` into Docker options and delegates session creation, cleanup, resume, and serialization to `DockerSandboxClient`. It intentionally keeps the `docker` backend ID and Docker state format. Use the built-in Docker client directly when you do not need an adapter; this package exists to demonstrate the external-package boundary. + +Pass a typed `ExampleDockerOptions` instance to this adapter. `SandboxRunConfig` interprets dictionary options using the `docker` backend ID, so the adapter rejects those and other Docker options before creating a session. The example options also reject unknown fields. Use `DockerSandboxClient` directly when configuring networking or other Docker-specific settings. + +## Install and run + +From the SDK repository root, with Python 3.10+, `uv`, and a running Docker daemon: + +```sh +uv venv /tmp/agents-provider-demo +uv pip install --python /tmp/agents-provider-demo/bin/python ./examples/sandbox/external_provider +docker pull python:3.14-slim +/tmp/agents-provider-demo/bin/python -m example_docker_provider.demo +``` + +On Windows, use the environment's `Scripts/python.exe` in place of `bin/python`. This example is not published on PyPI; install it from this directory. The package declares the SDK range `>=0.24.0,<0.25.0`; widen that range only after compatibility testing. To validate against your SDK checkout instead of a published SDK, install both local projects in the same command: + +```sh +uv pip install --python /tmp/agents-provider-demo/bin/python . ./examples/sandbox/external_provider +``` + +Expected output: + +```text +external provider works +``` + +The demo uses a real Docker container and a deterministic `ScriptedModel`. It needs no OpenAI API key, makes no model request, and disables trace export. Installing dependencies and pulling the image require network access. You can select an already available compatible image with `--image`; the image must supply the shell and filesystem tools required by the Docker backend. The default Docker networking behavior is unchanged. + +The runner owns the session created through `client=` and performs session cleanup. The application owns and closes the Docker SDK connection. The adapter returns the delegate's instrumented session unchanged, so it does not create a second session wrapper or a competing cleanup mechanism. + +See [Build a sandbox provider](../../../docs/sandbox/custom-providers.md) for the authoring contract, type registration, and the additional work required for a new execution runtime such as a micro-VM backend. diff --git a/examples/sandbox/external_provider/pyproject.toml b/examples/sandbox/external_provider/pyproject.toml new file mode 100644 index 0000000000..c709d3df67 --- /dev/null +++ b/examples/sandbox/external_provider/pyproject.toml @@ -0,0 +1,14 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "agents-example-docker-provider" +version = "0.1.0" +description = "An educational external sandbox client for the OpenAI Agents SDK" +readme = "README.md" +requires-python = ">=3.10" +dependencies = ["openai-agents[docker]>=0.24.0,<0.25.0"] + +[tool.hatch.build.targets.wheel] +packages = ["src/example_docker_provider"] diff --git a/examples/sandbox/external_provider/src/example_docker_provider/__init__.py b/examples/sandbox/external_provider/src/example_docker_provider/__init__.py new file mode 100644 index 0000000000..4273d812ae --- /dev/null +++ b/examples/sandbox/external_provider/src/example_docker_provider/__init__.py @@ -0,0 +1,70 @@ +"""A separately packaged client that delegates execution to the SDK's Docker backend.""" + +from typing import Literal + +from pydantic import ConfigDict, Field + +from agents.sandbox import Manifest +from agents.sandbox.sandboxes.docker import DockerSandboxClient, DockerSandboxClientOptions +from agents.sandbox.session import ( + BaseSandboxClient, + BaseSandboxClientOptions, + SandboxSession, + SandboxSessionState, +) +from agents.sandbox.snapshot import SnapshotBase, SnapshotSpec + +__all__ = ["ExampleDockerClient", "ExampleDockerOptions"] + + +class ExampleDockerOptions(BaseSandboxClientOptions): + """Options registered when the application imports this package.""" + + model_config = ConfigDict(extra="forbid") + + type: Literal["example_docker"] = "example_docker" + image: str = Field(min_length=1) + + +class ExampleDockerClient(BaseSandboxClient[ExampleDockerOptions]): + """Adapt package-specific options while retaining Docker sessions and state. + + This is a concrete external package, not a new execution engine. The application + owns the Docker connection supplied to the delegate and must close that connection. + """ + + # Backend identity describes the session runtime, not the distribution name. + backend_id = "docker" + + def __init__(self, delegate: DockerSandboxClient) -> None: + self.delegate = delegate + + async def create( + self, + *, + snapshot: SnapshotSpec | SnapshotBase | None = None, + manifest: Manifest | None = None, + options: ExampleDockerOptions, + ) -> SandboxSession: + if not isinstance(options, ExampleDockerOptions): + raise TypeError( + "ExampleDockerClient requires an ExampleDockerOptions instance; " + "use DockerSandboxClient directly for Docker options" + ) + return await self.delegate.create( + snapshot=snapshot, + manifest=manifest, + options=DockerSandboxClientOptions(image=options.image), + ) + + async def delete(self, session: SandboxSession) -> SandboxSession: + return await self.delegate.delete(session) + + async def resume(self, state: SandboxSessionState) -> SandboxSession: + return await self.delegate.resume(state) + + def serialize_session_state(self, state: SandboxSessionState) -> dict[str, object]: + return self.delegate.serialize_session_state(state) + + def deserialize_session_state(self, payload: dict[str, object]) -> SandboxSessionState: + return self.delegate.deserialize_session_state(payload) diff --git a/examples/sandbox/external_provider/src/example_docker_provider/demo.py b/examples/sandbox/external_provider/src/example_docker_provider/demo.py new file mode 100644 index 0000000000..1ed205474a --- /dev/null +++ b/examples/sandbox/external_provider/src/example_docker_provider/demo.py @@ -0,0 +1,60 @@ +"""Run a real container through an installed external client without a model API call.""" + +import argparse +import asyncio + +import docker + +from agents import RunConfig, Runner +from agents.sandbox import SandboxAgent, SandboxRunConfig +from agents.sandbox.capabilities import Shell +from agents.sandbox.sandboxes.docker import DockerSandboxClient +from agents.testing import ScriptedModel, assistant_message, function_call + +from . import ExampleDockerClient, ExampleDockerOptions + + +async def run(image: str) -> None: + connection = docker.from_env() + try: + client = ExampleDockerClient(DockerSandboxClient(connection)) + model = ScriptedModel( + [ + [ + function_call( + "exec_command", + {"cmd": "printf 'external provider works\\n'"}, + call_id="example-command", + ) + ], + [assistant_message("Demo complete.")], + ] + ) + agent = SandboxAgent(name="External provider demo", model=model, capabilities=[Shell()]) + result = await Runner.run( + agent, + "Run the example command.", + run_config=RunConfig( + tracing_disabled=True, + sandbox=SandboxRunConfig(client=client, options=ExampleDockerOptions(image=image)), + ), + ) + model.assert_complete() + # Inspect the actual tool output rather than trusting the scripted final response. + outputs = [item.output for item in result.new_items if item.type == "tool_call_output_item"] + if not any("external provider works" in str(output) for output in outputs): + raise RuntimeError(f"The container command did not succeed: {outputs!r}") + print("external provider works") + finally: + connection.close() + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--image", default="python:3.14-slim") + args = parser.parse_args() + asyncio.run(run(args.image)) + + +if __name__ == "__main__": + main() diff --git a/integration_tests/README.md b/integration_tests/README.md index 56188b2d17..ec7cca751b 100644 --- a/integration_tests/README.md +++ b/integration_tests/README.md @@ -8,7 +8,7 @@ Run the complete release-oriented matrix with: test "${OPENAI_API_KEY_SOURCE:-}" = service-account make integration-tests -`make integration-tests-release` runs the release-safe live matrix and the local Docker security contract in strict mode, so an unavailable daemon, image, credential, or required capability fails the release gate instead of becoming a skip. The focused `make integration-tests-security` target runs the same wheel and sdist security contract in strict mode without the live provider matrix; the security profile remains separate from the credential-free PR packaging job. The focused `make integration-tests-containers` target runs the packaged Docker sandbox security case together with the Dapr and Redis service integration once in a wheel environment. `make integration-tests-nightly` also includes extended capability and transport checks, while `make integration-tests-manual` includes checks reserved for an intentionally configured manual run. Focused entry points are `make integration-tests-packaging`, `make integration-tests-prospective-contract`, `make integration-tests-prospective-platform`, `make integration-tests-security`, `make integration-tests-containers`, `make integration-tests-mcp-v1`, `make integration-tests-core`, `make integration-tests-providers`, `make integration-tests-providers-external`, `make integration-tests-providers-all`, `make integration-tests-realtime`, `make integration-tests-voice`, `make integration-tests-hosted`, and `make integration-tests-extras`. The packaging profile validates the released public API manifest and historical `RunState` corpus from base wheel and sdist environments, then validates the public API again from wheel and sdist environments with the Cloudflare extra installed so dependency-conditional exports are required. The security profile installs the Docker extra for both distribution formats, checks packaged credential redaction, and runs model-controlled environment, filesystem, and process inspection inside a local Docker sandbox through the public `Runner` lifecycle. The containers profile installs the Dapr and Docker extras plus the pinned Testcontainers dependency without an OpenAI API credential. The MCP v1 profile installs the built wheel with both the supported v1 floor and latest tested v1 release in clean environments; the regular test job validates the locked MCP v2 dependency. +`make integration-tests-release` runs the release-safe live matrix and the local Docker security contract in strict mode, so an unavailable daemon, image, credential, or required capability fails the release gate instead of becoming a skip. The focused `make integration-tests-security` target runs the same wheel and sdist security contract in strict mode without the live provider matrix; the security profile remains separate from the credential-free PR packaging job. The focused `make integration-tests-containers` target runs the packaged Docker sandbox security case together with the Dapr and Redis service integration once in a wheel environment. `make integration-tests-nightly` also includes extended capability and transport checks, while `make integration-tests-manual` includes checks reserved for an intentionally configured manual run. Focused entry points are `make integration-tests-packaging`, `make integration-tests-prospective-contract`, `make integration-tests-prospective-platform`, `make integration-tests-security`, `make integration-tests-containers`, `make integration-tests-mcp-v1`, `make integration-tests-core`, `make integration-tests-providers`, `make integration-tests-providers-external`, `make integration-tests-providers-all`, `make integration-tests-realtime`, `make integration-tests-voice`, `make integration-tests-hosted`, and `make integration-tests-extras`. The packaging profile validates the released public API manifest and historical `RunState` corpus from base wheel and sdist environments, then validates the public API again from wheel and sdist environments with the Cloudflare extra installed so dependency-conditional exports are required. The security profile installs the Docker extra for both distribution formats, checks packaged credential redaction, and runs model-controlled environment, filesystem, and process inspection inside a local Docker sandbox through the public `Runner` lifecycle. The containers profile installs the Dapr and Docker extras plus the pinned Testcontainers dependency without an OpenAI API credential. The MCP v1 profile installs the built wheel with the supported security-patched v1 floor in a clean environment; the regular test job validates the locked MCP v2 dependency. Release PR preparation updates the rolling API manifest locally rather than in a credentialed GitHub workflow. After the release branch version bump, run `make update-released-api-contract VERSION=`, review and commit the manifest diff, then run `make check-released-api-contract VERSION=` after subsequent rebases. Promotion fails before writing if the candidate breaks the committed released contract. The prospective release-contract job performs this source validation in one dedicated Python process so provider behavior tests cannot change its import graph. Inspectable top-level classes and functions are promoted automatically; documented properties, intended submodule paths, and canonical aliases remain explicit review decisions recorded in the manifest. The packaged profiles remain the artifact-level verification that the committed contract holds for core and policy-declared optional surfaces across wheel, sdist, and supported platforms. diff --git a/integration_tests/packaging/test_distribution_contents.py b/integration_tests/packaging/test_distribution_contents.py index b965aac5b4..102169b079 100644 --- a/integration_tests/packaging/test_distribution_contents.py +++ b/integration_tests/packaging/test_distribution_contents.py @@ -11,6 +11,7 @@ from pathlib import Path import pytest +from packaging.requirements import Requirement pytestmark = pytest.mark.packaging @@ -63,6 +64,29 @@ def test_installed_distribution_advertises_expected_optional_extras() -> None: assert distribution.version +@pytest.mark.parametrize( + ("mcp_version", "allowed"), + [ + ("1.21.2", False), + ("1.29.1", False), + ("2.0.0", False), + ("2.1.1", False), + ("1.30.0", True), + ("2.2.0", True), + ("2.3.0", True), + ], +) +def test_installed_distribution_requires_patched_mcp(mcp_version: str, allowed: bool) -> None: + # GHSA-rwrf-2pqf-9j8j is fixed in MCP 1.30.0 and 2.2.0. + requirements = [ + Requirement(value) for value in importlib.metadata.requires("openai-agents") or [] + ] + mcp_requirement = next(requirement for requirement in requirements if requirement.name == "mcp") + + assert mcp_requirement.marker is None or mcp_requirement.marker.evaluate() + assert (mcp_version in mcp_requirement.specifier) is allowed + + @pytest.mark.parametrize( "module_name", [ diff --git a/mkdocs.yml b/mkdocs.yml index aa4d85a737..e9e0357e25 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -58,6 +58,7 @@ plugins: - Quickstart: sandbox_agents.md - Concepts: sandbox/guide.md - Sandbox clients: sandbox/clients.md + - Build a sandbox provider: sandbox/custom-providers.md - Agent memory: sandbox/memory.md - Realtime agents: - Quickstart: realtime/quickstart.md diff --git a/pyproject.toml b/pyproject.toml index 2fda55bf0d..8442326151 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,8 +13,10 @@ dependencies = [ "typing-extensions>=4.12.2, <5", "requests>=2.0, <3", "websockets>=15.0, <17", - "mcp>=1.19.0, <3; python_version >= '3.10'", + # GHSA-rwrf-2pqf-9j8j: retain patched MCP 1.x and 2.x releases. + "mcp>=1.30.0, <3, !=2.0.*, !=2.1.*; python_version >= '3.10'", # Security floors for transitive runtime dependencies. + "certifi>=2026.10.19", "httpx2>=2.12.0", "pyjwt>=2.15.0", "python-multipart>=0.0.31", @@ -58,7 +60,7 @@ dapr = [ "aiohttp>=3.14.3,<4", "protobuf>=5.29.6,!=6.30.*,!=6.31.*,!=6.32.*,!=6.33.0.*,!=6.33.1.*,!=6.33.2.*,!=6.33.3.*,!=6.33.4.*", ] -mongodb = ["pymongo>=4.14"] +mongodb = ["pymongo>=4.18.3"] docker = ["docker>=6.1"] blaxel = ["blaxel>=0.2.50", "aiohttp>=3.14.3,<4"] daytona = [ @@ -82,7 +84,6 @@ modal = [ "protobuf>=5.29.6,!=6.30.*,!=6.31.*,!=6.32.*,!=6.33.0.*,!=6.33.1.*,!=6.33.2.*,!=6.33.3.*,!=6.33.4.*", "pygments>=2.20.0", ] -runloop = ["runloop_api_client>=1.16.0,<2.0.0"] vercel = ["vercel-sandbox>=0.7.0,<0.9", "cbor2>=5.9.0"] s3 = ["boto3>=1.34"] temporal = [ @@ -95,7 +96,7 @@ temporal = [ [dependency-groups] dev = [ "mypy", - "ruff==0.16.8", + "ruff==0.17.1", "pytest>=9.0.3", "pytest-asyncio", "pytest-mock>=3.14.0", @@ -108,7 +109,7 @@ dev = [ "mkdocstrings[python]>=0.28.0", "mkdocs-static-i18n", "coverage>=7.6.12", - "playwright==1.50.0", + "playwright==1.63.0", "inline-snapshot>=0.20.7", "pynput", "types-pynput", @@ -127,7 +128,7 @@ dev = [ "protobuf>=5.29.6,!=6.30.*,!=6.31.*,!=6.32.*,!=6.33.0.*,!=6.33.1.*,!=6.33.2.*,!=6.33.3.*,!=6.33.4.*", "grpcio>=1.60.0", "pyright==1.1.408", - "pymongo>=4.14", + "pymongo>=4.18.3", "twilio>=9,<10", ] @@ -212,10 +213,6 @@ ignore_missing_imports = true module = ["daytona", "daytona.*"] ignore_missing_imports = true -[[tool.mypy.overrides]] -module = ["runloop_api_client", "runloop_api_client.*"] -ignore_missing_imports = true - [[tool.mypy.overrides]] module = ["blaxel", "blaxel.*"] ignore_missing_imports = true diff --git a/src/agents/_tool_invocation.py b/src/agents/_tool_invocation.py index f1be784b86..8685e3ec6e 100644 --- a/src/agents/_tool_invocation.py +++ b/src/agents/_tool_invocation.py @@ -2,7 +2,7 @@ import hashlib import json -from collections.abc import Mapping, Sequence +from collections.abc import Collection, Mapping, Sequence from typing import Any, TypeGuard from pydantic import BaseModel @@ -77,6 +77,16 @@ def _as_mapping(value: Any) -> Mapping[str, Any] | None: return None +def _has_other_model_type(value: Any, types: Collection[str]) -> bool: + """Return whether a model's ``type`` rules it out without dumping the whole model.""" + if not isinstance(value, BaseModel): + return False + value_type = getattr(value, "type", None) + return ( + isinstance(value_type, str) and value_type not in types and value_type != "hosted_tool_call" + ) + + def _normalize_value(value: Any, *, exclude_none: bool = False) -> Any: mapping = _as_mapping(value) if mapping is not None: @@ -160,9 +170,16 @@ def tool_invocation_identity_and_scope( invocation_role: str | None = None, ) -> tuple[str, str, str, str] | None: """Return invocation identity together with its stable approval scope.""" - call_identity = tool_invocation_call_id(raw_item) + if _has_other_model_type(raw_item, _TOOL_INVOCATION_TYPES): + return None + mapping = _unwrap_hosted_mcp_approval(raw_item) + if mapping is None: + return None + # Unwrapping is idempotent, so the helpers below can reuse the mapping instead of + # dumping the raw item again. + call_identity = tool_invocation_call_id(mapping) approval_scope_identity = tool_invocation_approval_scope( - raw_item, + mapping, tool_lookup_key=tool_lookup_key, tool_name=tool_name, invocation_role=invocation_role, @@ -174,10 +191,6 @@ def tool_invocation_identity_and_scope( if call_id is None or scope_invocation_type != invocation_type: return None - mapping = _unwrap_hosted_mcp_approval(raw_item) - if mapping is None: - return None - if invocation_type == "function_call": if "arguments" not in mapping: return None @@ -221,6 +234,8 @@ def tool_invocation_identity_and_scope( def tool_invocation_call_id(raw_item: Any) -> tuple[str, str | None] | None: """Return a recognized invocation type and its valid non-empty call ID, if present.""" + if _has_other_model_type(raw_item, _TOOL_INVOCATION_TYPES): + return None mapping = _unwrap_hosted_mcp_approval(raw_item) if mapping is None: return None @@ -297,6 +312,8 @@ def is_mcp_approval_invocation(raw_item: Any) -> bool: def tool_output_identity(raw_item: Any) -> tuple[str, str] | None: """Return the invocation type and call ID completed by a tool output item.""" + if _has_other_model_type(raw_item, _TOOL_OUTPUT_TYPES): + return None mapping = _as_mapping(raw_item) if mapping is None: return None diff --git a/src/agents/agent.py b/src/agents/agent.py index 3abbc8f92d..51b4a37336 100644 --- a/src/agents/agent.py +++ b/src/agents/agent.py @@ -2,11 +2,13 @@ import asyncio import contextvars +import copy import dataclasses import inspect from collections.abc import AsyncGenerator, Awaitable, Callable, Iterator, Sequence from contextlib import contextmanager from dataclasses import dataclass, field +from functools import wraps from typing import TYPE_CHECKING, Any, Generic, Literal, TypeAlias, cast from openai.types.responses.response_prompt_param import ResponsePromptParam @@ -177,10 +179,12 @@ def _initial_model_settings_for_model(model: str | Model | None) -> ModelSetting return ModelSettings() -def _model_settings_match_implicit_model_defaults( - model: str | Model | None, model_settings: ModelSettings -) -> bool: - return model_settings == _initial_model_settings_for_model(model) +@wraps(get_default_model_settings) +def _default_agent_model_settings(model: str | None = None) -> ModelSettings: + """Preserve the default factory's value and signature while recording SDK-owned defaults.""" + settings = get_default_model_settings(model) + settings.__dict__["_agent_default_settings"] = True + return settings @dataclass @@ -364,7 +368,7 @@ class Agent(AgentBase, Generic[TContext]): `agents.models.get_default_model()` (currently "gpt-5.6-luna"). """ - model_settings: ModelSettings = field(default_factory=get_default_model_settings) + model_settings: ModelSettings = field(default_factory=_default_agent_model_settings) """Configures model-specific tuning parameters (e.g. temperature, top_p). Accepts a ``ModelSettings`` instance or a dictionary containing its fields. @@ -513,9 +517,14 @@ def __post_init__(self): parameter_name="Agent model_settings", model_settings_type=_declared_model_settings_type(type(self), "model_settings"), ) - - if self.model is not None and self.model_settings == get_default_model_settings(): + settings_omitted = self.model_settings.__dict__.get("_agent_default_settings") is True + if settings_omitted: self.model_settings = _initial_model_settings_for_model(self.model) + self.model_settings.__dict__["_agent_defaults_untouched"] = True + # Keep caller assignment and in-place edits from being treated as implicit defaults. + self._implicit_model_settings: tuple[ModelSettings, ModelSettings] | None = ( + (self.model_settings, copy.deepcopy(self.model_settings)) if settings_omitted else None + ) if not isinstance(self.input_guardrails, list): raise TypeError( @@ -588,11 +597,10 @@ def clone(self, **kwargs: Any) -> Agent[TContext]: new_agent = agent.clone(instructions="New instructions") ``` """ - if ( - "model" in kwargs - and "model_settings" not in kwargs - and _model_settings_match_implicit_model_defaults(self.model, self.model_settings) - ): + inherit_implicit_settings = ( + "model_settings" not in kwargs and self._uses_implicit_model_settings() + ) + if inherit_implicit_settings and "model" in kwargs: kwargs["model_settings"] = _initial_model_settings_for_model(kwargs["model"]) if "model_settings" in kwargs: kwargs["model_settings"] = _coerce_model_settings( @@ -601,7 +609,23 @@ def clone(self, **kwargs: Any) -> Agent[TContext]: model_settings_type=type(self.model_settings), inherited_model_settings=self.model_settings, ) - return dataclasses.replace(self, **kwargs) + cloned = dataclasses.replace(self, **kwargs) + if inherit_implicit_settings: + cloned.model_settings.__dict__["_agent_defaults_untouched"] = True + cloned._implicit_model_settings = ( + cloned.model_settings, + copy.deepcopy(cloned.model_settings), + ) + return cloned + + def _uses_implicit_model_settings(self) -> bool: + defaults = self._implicit_model_settings + return ( + defaults is not None + and self.model_settings is defaults[0] + and self.model_settings.__dict__.get("_agent_defaults_untouched") is True + and self.model_settings == defaults[1] + ) def as_tool( self, diff --git a/src/agents/extensions/models/any_llm_model.py b/src/agents/extensions/models/any_llm_model.py index 9a5200a567..762e7dc8c2 100644 --- a/src/agents/extensions/models/any_llm_model.py +++ b/src/agents/extensions/models/any_llm_model.py @@ -40,6 +40,7 @@ from ...models.chatcmpl_converter import Converter from ...models.chatcmpl_helpers import HEADERS, HEADERS_OVERRIDE, ChatCmplHelpers from ...models.chatcmpl_stream_handler import ChatCmplStreamHandler +from ...models.default_models import get_default_model_settings from ...models.fake_id import FAKE_RESPONSES_ID from ...models.interface import Model, ModelTracing from ...models.openai_responses import ( @@ -267,6 +268,14 @@ def __init__( self._provider_name, self._provider_model = self._split_model_name(model) self._provider_cache: dict[bool, Any] = {} + def get_default_model_settings(self) -> ModelSettings: + """Use the parsed provider model rather than the namespaced routing identifier.""" + if self._provider_name == "openai": + return get_default_model_settings(self._provider_model) + if self._provider_name == "openrouter" and self._provider_model.startswith("openai/"): + return get_default_model_settings(self._provider_model.removeprefix("openai/")) + return ModelSettings() + def get_retry_advice(self, request: ModelRetryAdviceRequest) -> ModelRetryAdvice | None: return get_openai_retry_advice(request) diff --git a/src/agents/extensions/models/litellm_model.py b/src/agents/extensions/models/litellm_model.py index d452946f10..668600b6b5 100644 --- a/src/agents/extensions/models/litellm_model.py +++ b/src/agents/extensions/models/litellm_model.py @@ -49,6 +49,7 @@ from ...models.chatcmpl_converter import Converter from ...models.chatcmpl_helpers import HEADERS, HEADERS_OVERRIDE, ChatCmplHelpers from ...models.chatcmpl_stream_handler import ChatCmplStreamHandler +from ...models.default_models import get_default_model_settings from ...models.fake_id import FAKE_RESPONSES_ID from ...models.interface import Model, ModelTracing from ...models.openai_responses import Converter as OpenAIResponsesConverter @@ -162,6 +163,11 @@ def __init__( self.api_key = api_key self.should_replay_reasoning_content = should_replay_reasoning_content + def get_default_model_settings(self) -> ModelSettings: + """Recognize OpenAI model IDs carried by the supported routing namespaces.""" + model_name = self.model.removeprefix("openai/").removeprefix("openrouter/openai/") + return get_default_model_settings(model_name) + def get_retry_advice(self, request: ModelRetryAdviceRequest) -> ModelRetryAdvice | None: # LiteLLM exceptions mirror OpenAI-style status/header fields. # Reuse the same normalization to expose retry-after and explicit retry/no-retry hints. diff --git a/src/agents/extensions/sandbox/__init__.py b/src/agents/extensions/sandbox/__init__.py index 9ea2eb8dc3..82e754aa2b 100644 --- a/src/agents/extensions/sandbox/__init__.py +++ b/src/agents/extensions/sandbox/__init__.py @@ -74,30 +74,6 @@ except Exception: # pragma: no cover _HAS_CLOUDFLARE = False -try: - from .runloop import ( - DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT as DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT, - DEFAULT_RUNLOOP_WORKSPACE_ROOT as DEFAULT_RUNLOOP_WORKSPACE_ROOT, - RunloopAfterIdle as RunloopAfterIdle, - RunloopCloudBucketMountStrategy as RunloopCloudBucketMountStrategy, - RunloopExistingSecret as RunloopExistingSecret, - RunloopGatewaySpec as RunloopGatewaySpec, - RunloopLaunchParameters as RunloopLaunchParameters, - RunloopMcpSpec as RunloopMcpSpec, - RunloopPlatformClient as RunloopPlatformClient, - RunloopSandboxClient as RunloopSandboxClient, - RunloopSandboxClientOptions as RunloopSandboxClientOptions, - RunloopSandboxSession as RunloopSandboxSession, - RunloopSandboxSessionState as RunloopSandboxSessionState, - RunloopTimeouts as RunloopTimeouts, - RunloopTunnelConfig as RunloopTunnelConfig, - RunloopUserParameters as RunloopUserParameters, - ) - - _HAS_RUNLOOP = True -except Exception: # pragma: no cover - _HAS_RUNLOOP = False - try: from .vercel import ( VercelCloudBucketMountStrategy as VercelCloudBucketMountStrategy, @@ -189,25 +165,3 @@ "VercelSandboxSessionState", ] ) - -if _HAS_RUNLOOP: - __all__.extend( - [ - "DEFAULT_RUNLOOP_WORKSPACE_ROOT", - "DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT", - "RunloopAfterIdle", - "RunloopExistingSecret", - "RunloopGatewaySpec", - "RunloopLaunchParameters", - "RunloopMcpSpec", - "RunloopPlatformClient", - "RunloopCloudBucketMountStrategy", - "RunloopSandboxClient", - "RunloopSandboxClientOptions", - "RunloopSandboxSession", - "RunloopSandboxSessionState", - "RunloopTimeouts", - "RunloopTunnelConfig", - "RunloopUserParameters", - ] - ) diff --git a/src/agents/extensions/sandbox/runloop/__init__.py b/src/agents/extensions/sandbox/runloop/__init__.py deleted file mode 100644 index 9070307bd0..0000000000 --- a/src/agents/extensions/sandbox/runloop/__init__.py +++ /dev/null @@ -1,53 +0,0 @@ -from __future__ import annotations - -from .mounts import RunloopCloudBucketMountStrategy -from .sandbox import ( - DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT, - DEFAULT_RUNLOOP_WORKSPACE_ROOT, - RunloopAfterIdle, - RunloopExistingSecret, - RunloopGatewaySpec, - RunloopLaunchParameters, - RunloopMcpSpec, - RunloopPlatformAxonsClient, - RunloopPlatformBenchmarksClient, - RunloopPlatformBlueprintsClient, - RunloopPlatformClient, - RunloopPlatformNetworkPoliciesClient, - RunloopPlatformSecretsClient, - RunloopSandboxClient, - RunloopSandboxClientOptions, - RunloopSandboxSession, - RunloopSandboxSessionState, - RunloopTimeouts, - RunloopTunnelConfig, - RunloopUserParameters, - _decode_runloop_snapshot_ref, - _encode_runloop_snapshot_ref, -) - -__all__ = [ - "DEFAULT_RUNLOOP_WORKSPACE_ROOT", - "DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT", - "RunloopAfterIdle", - "RunloopExistingSecret", - "RunloopGatewaySpec", - "RunloopLaunchParameters", - "RunloopMcpSpec", - "RunloopPlatformAxonsClient", - "RunloopPlatformBenchmarksClient", - "RunloopPlatformBlueprintsClient", - "RunloopPlatformClient", - "RunloopPlatformNetworkPoliciesClient", - "RunloopPlatformSecretsClient", - "RunloopCloudBucketMountStrategy", - "RunloopSandboxClient", - "RunloopSandboxClientOptions", - "RunloopSandboxSession", - "RunloopSandboxSessionState", - "RunloopTimeouts", - "RunloopTunnelConfig", - "RunloopUserParameters", - "_decode_runloop_snapshot_ref", - "_encode_runloop_snapshot_ref", -] diff --git a/src/agents/extensions/sandbox/runloop/mounts.py b/src/agents/extensions/sandbox/runloop/mounts.py deleted file mode 100644 index 3fde66171d..0000000000 --- a/src/agents/extensions/sandbox/runloop/mounts.py +++ /dev/null @@ -1,203 +0,0 @@ -"""Mount strategy for Runloop sandboxes.""" - -from __future__ import annotations - -from pathlib import Path -from typing import Literal - -from ....sandbox._mount_security import ( - redact_mount_error_data, - validate_mount_activation_credential_boundary, -) -from ....sandbox.entries.mounts.base import InContainerMountStrategy, Mount, MountStrategyBase -from ....sandbox.entries.mounts.patterns import RcloneMountPattern -from ....sandbox.errors import MountConfigError -from ....sandbox.materialization import MaterializedFile -from ....sandbox.session.base_sandbox_session import BaseSandboxSession -from .._rclone import ( - ensure_rclone as _ensure_rclone, - rclone_pattern_for_session as _rclone_pattern_for_session, -) - -_APT = "DEBIAN_FRONTEND=noninteractive DEBCONF_NOWARNINGS=yes apt-get -o Dpkg::Use-Pty=0" -_INSTALL_FUSE_COMMANDS = ( - f"{_APT} update -qq", - f"{_APT} install -y -qq fuse3", -) -_FUSE_ALLOW_OTHER = ( - "chmod a+rw /dev/fuse && " - "touch /etc/fuse.conf && " - "(grep -qxF user_allow_other /etc/fuse.conf || " - "printf '\\nuser_allow_other\\n' >> /etc/fuse.conf)" -) - - -async def _ensure_fuse_support(session: BaseSandboxSession) -> None: - dev_fuse = await session.exec("sh", "-lc", "test -c /dev/fuse", shell=False) - if not dev_fuse.ok(): - raise MountConfigError( - message="Runloop cloud bucket mounts require FUSE support", - context={"missing": "/dev/fuse"}, - ) - - kmod = await session.exec("sh", "-lc", "grep -qw fuse /proc/filesystems", shell=False) - if not kmod.ok(): - raise MountConfigError( - message="Runloop cloud bucket mounts require FUSE support", - context={"missing": "fuse in /proc/filesystems"}, - ) - - fusermount = await session.exec( - "sh", - "-lc", - "command -v fusermount3 >/dev/null 2>&1 || command -v fusermount >/dev/null 2>&1", - shell=False, - ) - if not fusermount.ok(): - apt = await session.exec("sh", "-lc", "command -v apt-get >/dev/null 2>&1", shell=False) - if not apt.ok(): - raise MountConfigError( - message="fusermount is not installed and apt-get is unavailable; preinstall fuse3", - context={"package": "fuse3"}, - ) - for command in _INSTALL_FUSE_COMMANDS: - install = await session.exec( - "sh", - "-lc", - command, - shell=False, - timeout=300, - user="root", - ) - if not install.ok(): - raise MountConfigError( - message="failed to install fuse3", - context={"package": "fuse3", "exit_code": install.exit_code}, - ) - - fusermount = await session.exec( - "sh", - "-lc", - "command -v fusermount3 >/dev/null 2>&1 || command -v fusermount >/dev/null 2>&1", - shell=False, - ) - if not fusermount.ok(): - raise MountConfigError( - message="fuse3 was installed but fusermount is still not available", - context={"package": "fuse3"}, - ) - - chmod_result = await session.exec( - "sh", - "-lc", - _FUSE_ALLOW_OTHER, - shell=False, - timeout=30, - user="root", - ) - if not chmod_result.ok(): - raise MountConfigError( - message="failed to make /dev/fuse accessible", - context={"exit_code": chmod_result.exit_code}, - ) - - -def _assert_runloop_session(session: BaseSandboxSession) -> None: - if type(session).__name__ != "RunloopSandboxSession": - raise MountConfigError( - message="runloop cloud bucket mounts require a RunloopSandboxSession", - context={"session_type": type(session).__name__}, - ) - - -class RunloopCloudBucketMountStrategy(MountStrategyBase): - """Mount rclone-backed cloud storage in Runloop sandboxes.""" - - type: Literal["runloop_cloud_bucket"] = "runloop_cloud_bucket" - pattern: RcloneMountPattern = RcloneMountPattern(mode="fuse") - - def _delegate(self) -> InContainerMountStrategy: - return InContainerMountStrategy(pattern=self.pattern) - - async def _delegate_for_session(self, session: BaseSandboxSession) -> InContainerMountStrategy: - return InContainerMountStrategy( - pattern=await _rclone_pattern_for_session(session, self.pattern) - ) - - def validate_mount(self, mount: Mount) -> None: - self._delegate().validate_mount(mount) - - @redact_mount_error_data - async def activate( - self, - mount: Mount, - session: BaseSandboxSession, - dest: Path, - base_dir: Path, - ) -> list[MaterializedFile]: - validate_mount_activation_credential_boundary( - mount, - self, - manifest=getattr(getattr(session, "state", None), "manifest", None), - mount_path=lambda: mount._resolve_mount_path(session, dest), - provider_backend_id="runloop", - ) - _assert_runloop_session(session) - if self.pattern.mode == "fuse": - await _ensure_fuse_support(session) - await _ensure_rclone(session) - delegate = await self._delegate_for_session(session) - return await delegate.activate(mount, session, dest, base_dir) - - @redact_mount_error_data - async def deactivate( - self, - mount: Mount, - session: BaseSandboxSession, - dest: Path, - base_dir: Path, - ) -> None: - _assert_runloop_session(session) - await self._delegate().deactivate(mount, session, dest, base_dir) - - @redact_mount_error_data - async def teardown_for_snapshot( - self, - mount: Mount, - session: BaseSandboxSession, - path: Path, - ) -> None: - _assert_runloop_session(session) - await self._delegate().teardown_for_snapshot(mount, session, path) - - @redact_mount_error_data - async def restore_after_snapshot( - self, - mount: Mount, - session: BaseSandboxSession, - path: Path, - ) -> None: - validate_mount_activation_credential_boundary( - mount, - self, - manifest=getattr(getattr(session, "state", None), "manifest", None), - mount_path=path, - provider_backend_id="runloop", - ) - _assert_runloop_session(session) - if self.pattern.mode == "fuse": - await _ensure_fuse_support(session) - await _ensure_rclone(session) - delegate = await self._delegate_for_session(session) - await delegate.restore_after_snapshot(mount, session, path) - - def build_docker_volume_driver_config( - self, - mount: Mount, - ) -> tuple[str, dict[str, str], bool] | None: - return None - - -__all__ = [ - "RunloopCloudBucketMountStrategy", -] diff --git a/src/agents/extensions/sandbox/runloop/sandbox.py b/src/agents/extensions/sandbox/runloop/sandbox.py deleted file mode 100644 index 2d3477218c..0000000000 --- a/src/agents/extensions/sandbox/runloop/sandbox.py +++ /dev/null @@ -1,1763 +0,0 @@ -""" -Runloop sandbox (https://runloop.ai) implementation. - -This module provides a Runloop-backed sandbox client/session implementation backed by -`runloop_api_client.sdk.AsyncRunloopSDK`. - -The `runloop_api_client` dependency is optional, so package-level exports should guard imports of -this module. Within this module, Runloop SDK imports are lazy so users without the extra can still -import the package. -""" - -from __future__ import annotations - -import asyncio -import base64 -import io -import json -import logging -import posixpath -import shlex -import uuid -from collections.abc import Sequence -from dataclasses import dataclass -from pathlib import Path, PurePosixPath -from typing import TYPE_CHECKING, Any, Literal, cast -from urllib.parse import urlsplit - -from pydantic import BaseModel, Field -from runloop_api_client.types import ( - AfterIdle as _RunloopSdkAfterIdle, - LaunchParameters as _RunloopSdkLaunchParameters, -) -from runloop_api_client.types.shared.launch_parameters import ( - UserParameters as _RunloopSdkUserParameters, -) - -from ....sandbox._mount_security import redact_mount_error_data -from ....sandbox.entries import Mount -from ....sandbox.errors import ( - ExecTimeoutError, - ExecTransportError, - ExposedPortUnavailableError, - WorkspaceArchiveReadError, - WorkspaceArchiveWriteError, - WorkspaceReadNotFoundError, - WorkspaceWriteTypeError, -) -from ....sandbox.manifest import Manifest -from ....sandbox.session import SandboxSession, SandboxSessionState -from ....sandbox.session.base_sandbox_session import BaseSandboxSession -from ....sandbox.session.bounded_read import collect_bounded -from ....sandbox.session.dependencies import Dependencies -from ....sandbox.session.manager import Instrumentation -from ....sandbox.session.runtime_helpers import RESOLVE_WORKSPACE_PATH_HELPER, RuntimeHelperScript -from ....sandbox.session.sandbox_client import BaseSandboxClient, BaseSandboxClientOptions -from ....sandbox.snapshot import SnapshotBase, SnapshotSpec, resolve_snapshot -from ....sandbox.types import ExecResult, ExposedPortEndpoint, User -from ....sandbox.util.retry import iter_exception_chain -from ....sandbox.util.tar_utils import UnsafeTarMemberError, validate_tar_bytes -from ....sandbox.workspace_paths import coerce_posix_path, posix_path_as_path, sandbox_path_str - -if TYPE_CHECKING: - from runloop_api_client.sdk.async_execution_result import ( - AsyncExecutionResult as RunloopAsyncExecutionResult, - ) - from runloop_api_client.sdk.async_snapshot import AsyncSnapshot as RunloopAsyncSnapshot - from runloop_api_client.types.devbox_view import DevboxView as RunloopDevboxView - -DEFAULT_RUNLOOP_WORKSPACE_ROOT = "/home/user" -DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT = "/root" -_RUNLOOP_DEFAULT_HOME = PurePosixPath("/home/user") -_RUNLOOP_ROOT_HOME = PurePosixPath("/root") -_RUNLOOP_SANDBOX_SNAPSHOT_MAGIC = b"RUNLOOP_SANDBOX_SNAPSHOT_V1\n" - -logger = logging.getLogger(__name__) - -RunloopAfterIdle = _RunloopSdkAfterIdle -RunloopLaunchParameters = _RunloopSdkLaunchParameters -RunloopUserParameters = _RunloopSdkUserParameters - - -@dataclass(frozen=True) -class _RunloopSdkImports: - async_sdk: type[Any] - api_connection_error: type[BaseException] - api_response_validation_error: type[BaseException] - api_status_error: type[BaseException] - api_timeout_error: type[BaseException] - authentication_error: type[BaseException] - bad_request_error: type[BaseException] - internal_server_error: type[BaseException] - not_found_error: type[BaseException] - permission_denied_error: type[BaseException] - polling_config: type[Any] | None - polling_timeout: type[BaseException] | None - rate_limit_error: type[BaseException] - runloop_error: type[BaseException] - unprocessable_entity_error: type[BaseException] - - -_RUNLOOP_SDK_IMPORTS: _RunloopSdkImports | None = None - - -def _import_runloop_sdk() -> _RunloopSdkImports: - global _RUNLOOP_SDK_IMPORTS - if _RUNLOOP_SDK_IMPORTS is not None: - return _RUNLOOP_SDK_IMPORTS - - try: - from runloop_api_client import ( - APIConnectionError, - APIResponseValidationError, - APIStatusError, - APITimeoutError, - AuthenticationError, - BadRequestError, - InternalServerError, - NotFoundError, - PermissionDeniedError, - RateLimitError, - RunloopError, - UnprocessableEntityError, - ) - from runloop_api_client.sdk import AsyncRunloopSDK - except ImportError as e: - raise ImportError( - "RunloopSandboxClient requires the optional `runloop_api_client` dependency.\n" - "Install the Runloop extra before using this sandbox backend." - ) from e - - polling_config: type[Any] | None = None - polling_timeout: type[BaseException] | None = None - try: - from runloop_api_client.lib.polling import ( - PollingConfig as RunloopPollingConfig, - PollingTimeout as RunloopPollingTimeout, - ) - except ImportError: - pass - else: - polling_config = RunloopPollingConfig - polling_timeout = RunloopPollingTimeout - - _RUNLOOP_SDK_IMPORTS = _RunloopSdkImports( - async_sdk=AsyncRunloopSDK, - api_connection_error=APIConnectionError, - api_response_validation_error=APIResponseValidationError, - api_status_error=APIStatusError, - api_timeout_error=APITimeoutError, - authentication_error=AuthenticationError, - bad_request_error=BadRequestError, - internal_server_error=InternalServerError, - not_found_error=NotFoundError, - permission_denied_error=PermissionDeniedError, - polling_config=polling_config, - polling_timeout=polling_timeout, - rate_limit_error=RateLimitError, - runloop_error=RunloopError, - unprocessable_entity_error=UnprocessableEntityError, - ) - return _RUNLOOP_SDK_IMPORTS - - -def _encode_runloop_snapshot_ref(*, snapshot_id: str) -> bytes: - body = json.dumps({"snapshot_id": snapshot_id}, separators=(",", ":"), sort_keys=True).encode( - "utf-8" - ) - return _RUNLOOP_SANDBOX_SNAPSHOT_MAGIC + body - - -def _decode_runloop_snapshot_ref(raw: bytes) -> str | None: - if not raw.startswith(_RUNLOOP_SANDBOX_SNAPSHOT_MAGIC): - return None - body = raw[len(_RUNLOOP_SANDBOX_SNAPSHOT_MAGIC) :] - try: - obj = json.loads(body.decode("utf-8")) - except (UnicodeDecodeError, json.JSONDecodeError): - return None - snapshot_id = obj.get("snapshot_id") if isinstance(obj, dict) else None - return snapshot_id if isinstance(snapshot_id, str) and snapshot_id else None - - -def _runloop_json_safe_body(body: object) -> tuple[str, object] | None: - if isinstance(body, str | int | float | bool) or body is None: - return ("provider_body", body) - if isinstance(body, dict | list): - try: - json.dumps(body) - except TypeError: - return ("provider_body_repr", repr(body)) - return ("provider_body", body) - return ("provider_body_repr", repr(body)) - - -def _runloop_error_context( - exc: BaseException, - *, - backend_detail: str | None = None, -) -> dict[str, object]: - context: dict[str, object] = { - "backend": "runloop", - "cause_type": type(exc).__name__, - } - if backend_detail is not None: - context["detail"] = backend_detail - - message = getattr(exc, "message", None) - if isinstance(message, str) and message: - context["provider_message"] = message - else: - provider_message = str(exc) - if provider_message: - context["provider_message"] = provider_message - - status_code = getattr(exc, "status_code", None) - response = getattr(exc, "response", None) - if not isinstance(status_code, int): - response_status = getattr(response, "status_code", None) - if isinstance(response_status, int): - status_code = response_status - if isinstance(status_code, int): - context["http_status"] = status_code - - request = getattr(exc, "request", None) - request_url = getattr(request, "url", None) - if request_url is not None: - context["request_url"] = str(request_url) - request_method = getattr(request, "method", None) - if isinstance(request_method, str) and request_method: - context["request_method"] = request_method - - if hasattr(exc, "body"): - safe_body = _runloop_json_safe_body(getattr(exc, "body", None)) - if safe_body is not None: - context[safe_body[0]] = safe_body[1] - - return context - - -def _is_runloop_timeout(exc: BaseException) -> bool: - polling_timeout = _import_runloop_sdk().polling_timeout - if polling_timeout is not None and isinstance(exc, polling_timeout): - return True - if isinstance(exc, _import_runloop_sdk().api_timeout_error): - return True - if isinstance(exc, _import_runloop_sdk().api_status_error): - status_code = getattr(exc, "status_code", None) - response = getattr(exc, "response", None) - if not isinstance(status_code, int): - response_status = getattr(response, "status_code", None) - if isinstance(response_status, int): - status_code = response_status - return status_code == 408 - return False - - -def _runloop_status_code(exc: BaseException) -> int | None: - status_code = getattr(exc, "status_code", None) - response = getattr(exc, "response", None) - if not isinstance(status_code, int): - response_status = getattr(response, "status_code", None) - if isinstance(response_status, int): - status_code = response_status - return status_code if isinstance(status_code, int) else None - - -def _runloop_error_message(exc: BaseException) -> str | None: - body = getattr(exc, "body", None) - if isinstance(body, dict): - message = body.get("message") or body.get("error") - if isinstance(message, str) and message: - return message - - message = getattr(exc, "message", None) - if isinstance(message, str) and message: - return message - - if exc.args: - first = exc.args[0] - if isinstance(first, str) and first: - return first - - return None - - -_RUNLOOP_HTTP_STATUS_RETRYABLE: dict[int, bool] = { - 400: False, - 401: False, - 403: False, - 404: False, - 408: True, - 422: False, - 429: True, - 500: True, - 502: True, - 503: True, - 504: True, -} - - -def _runloop_retryable_error_types() -> tuple[type[BaseException], ...]: - sdk_imports = _import_runloop_sdk() - return ( - sdk_imports.api_connection_error, - sdk_imports.api_timeout_error, - sdk_imports.internal_server_error, - sdk_imports.rate_limit_error, - ) - - -def _runloop_non_retryable_error_types() -> tuple[type[BaseException], ...]: - sdk_imports = _import_runloop_sdk() - return ( - sdk_imports.authentication_error, - sdk_imports.bad_request_error, - sdk_imports.not_found_error, - sdk_imports.permission_denied_error, - sdk_imports.unprocessable_entity_error, - ) - - -def _runloop_provider_retryability(exc: BaseException) -> bool | None: - retryable_error_types = _runloop_retryable_error_types() - non_retryable_error_types = _runloop_non_retryable_error_types() - for candidate in iter_exception_chain(exc): - if isinstance(candidate, retryable_error_types): - return True - if isinstance(candidate, non_retryable_error_types): - return False - status_code = _runloop_status_code(candidate) - if status_code in _RUNLOOP_HTTP_STATUS_RETRYABLE: - return _RUNLOOP_HTTP_STATUS_RETRYABLE[status_code] - return None - - -def _runloop_provider_error_types() -> tuple[type[BaseException], ...]: - sdk_imports = _import_runloop_sdk() - return ( - sdk_imports.api_connection_error, - sdk_imports.api_response_validation_error, - sdk_imports.api_status_error, - sdk_imports.runloop_error, - ) - - -def _is_runloop_not_found(exc: BaseException) -> bool: - return isinstance(exc, _import_runloop_sdk().not_found_error) - - -def _is_runloop_conflict(exc: BaseException) -> bool: - if not isinstance(exc, _import_runloop_sdk().api_status_error): - return False - - status_code = _runloop_status_code(exc) - if status_code == 409: - return True - - message = _runloop_error_message(exc) - if status_code == 400 and isinstance(message, str): - return "already exists" in message.lower() - - return False - - -def _runloop_polling_config(*, timeout_s: float | None) -> object | None: - if timeout_s is None: - return None - polling_config = _import_runloop_sdk().polling_config - if polling_config is None: - return None - return cast(object, polling_config(timeout_seconds=max(float(timeout_s), 0.001))) - - -def _is_runloop_provider_error(exc: BaseException) -> bool: - return isinstance( - exc, - _runloop_provider_error_types(), - ) - - -class RunloopTimeouts(BaseModel): - """Timeout configuration for Runloop sandbox operations.""" - - model_config = {"frozen": True} - - exec_timeout_unbounded_s: float = Field(default=24 * 60 * 60, ge=1) - create_s: float = Field(default=300.0, ge=1) - keepalive_s: float = Field(default=10.0, ge=1) - cleanup_s: float = Field(default=30.0, ge=1) - fast_op_s: float = Field(default=30.0, ge=1) - file_upload_s: float = Field(default=1800.0, ge=1) - file_download_s: float = Field(default=1800.0, ge=1) - snapshot_s: float = Field(default=300.0, ge=1) - suspend_s: float = Field(default=120.0, ge=1) - resume_s: float = Field(default=300.0, ge=1) - - -class RunloopTunnelConfig(BaseModel): - """Runloop public tunnel configuration.""" - - model_config = {"frozen": True} - - auth_mode: Literal["open", "authenticated"] | None = None - http_keep_alive: bool | None = None - wake_on_http: bool | None = None - - -class RunloopGatewaySpec(BaseModel): - """Runloop agent gateway binding.""" - - model_config = {"frozen": True} - - gateway: str = Field(min_length=1) - secret: str = Field(min_length=1) - - -class RunloopMcpSpec(BaseModel): - """Runloop MCP gateway binding.""" - - model_config = {"frozen": True} - - mcp_config: str = Field(min_length=1) - secret: str = Field(min_length=1) - - -class RunloopExistingSecret(BaseModel): - """A `managed_secrets` entry that is already stored on the Runloop account. - - Used in place of a secret value to attach an account secret to the devbox by name, - leaving the stored value untouched. - """ - - model_config = {"frozen": True, "extra": "forbid"} - - -def _normalize_runloop_user_parameters( - user_parameters: RunloopUserParameters | dict[str, object] | None, -) -> RunloopUserParameters | None: - if isinstance(user_parameters, RunloopUserParameters): - return user_parameters - if user_parameters is None: - return None - if isinstance(user_parameters, BaseModel): - return RunloopUserParameters.model_validate(user_parameters.model_dump(mode="json")) - return RunloopUserParameters.model_validate(user_parameters) - - -def _normalize_runloop_launch_parameters( - launch_parameters: RunloopLaunchParameters | dict[str, object] | None, -) -> RunloopLaunchParameters | None: - if isinstance(launch_parameters, RunloopLaunchParameters): - return launch_parameters - if launch_parameters is None: - return None - if isinstance(launch_parameters, BaseModel): - return RunloopLaunchParameters.model_validate(launch_parameters.model_dump(mode="json")) - return RunloopLaunchParameters.model_validate(launch_parameters) - - -def _normalize_runloop_tunnel_config( - tunnel: RunloopTunnelConfig | dict[str, object] | None, -) -> RunloopTunnelConfig | None: - if isinstance(tunnel, RunloopTunnelConfig): - return tunnel - if tunnel is None: - return None - if isinstance(tunnel, BaseModel): - return RunloopTunnelConfig.model_validate(tunnel.model_dump(mode="json")) - return RunloopTunnelConfig.model_validate(tunnel) - - -class RunloopSandboxClientOptions(BaseSandboxClientOptions): - """Client options for the Runloop sandbox.""" - - type: Literal["runloop"] = "runloop" - blueprint_id: str | None = None - blueprint_name: str | None = None - env_vars: dict[str, str] | None = None - pause_on_exit: bool = False - name: str | None = None - timeouts: RunloopTimeouts | dict[str, object] | None = None - exposed_ports: tuple[int, ...] = () - user_parameters: RunloopUserParameters | dict[str, object] | None = None - launch_parameters: RunloopLaunchParameters | dict[str, object] | None = None - tunnel: RunloopTunnelConfig | dict[str, object] | None = None - gateways: dict[str, RunloopGatewaySpec] | None = None - mcp: dict[str, RunloopMcpSpec] | None = None - metadata: dict[str, str] | None = None - managed_secrets: dict[str, str | RunloopExistingSecret] | None = None - - def __init__( - self, - blueprint_id: str | None = None, - blueprint_name: str | None = None, - env_vars: dict[str, str] | None = None, - pause_on_exit: bool = False, - name: str | None = None, - timeouts: RunloopTimeouts | dict[str, object] | None = None, - exposed_ports: tuple[int, ...] = (), - user_parameters: RunloopUserParameters | dict[str, object] | None = None, - launch_parameters: RunloopLaunchParameters | dict[str, object] | None = None, - tunnel: RunloopTunnelConfig | dict[str, object] | None = None, - gateways: dict[str, RunloopGatewaySpec] | None = None, - mcp: dict[str, RunloopMcpSpec] | None = None, - metadata: dict[str, str] | None = None, - managed_secrets: dict[str, str | RunloopExistingSecret] | None = None, - *, - type: Literal["runloop"] = "runloop", - ) -> None: - super().__init__( - type=type, - blueprint_id=blueprint_id, - blueprint_name=blueprint_name, - env_vars=env_vars, - pause_on_exit=pause_on_exit, - name=name, - timeouts=timeouts, - exposed_ports=exposed_ports, - user_parameters=user_parameters, - launch_parameters=launch_parameters, - tunnel=tunnel, - gateways=gateways, - mcp=mcp, - metadata=metadata, - managed_secrets=managed_secrets, - ) - - -class RunloopSandboxSessionState(SandboxSessionState): - """Serializable state for a Runloop-backed session.""" - - type: Literal["runloop"] = "runloop" - devbox_id: str - blueprint_id: str | None = None - blueprint_name: str | None = None - base_env_vars: dict[str, str] = Field(default_factory=dict) - pause_on_exit: bool = False - name: str | None = None - timeouts: RunloopTimeouts = Field(default_factory=RunloopTimeouts) - user_parameters: RunloopUserParameters | None = None - launch_parameters: RunloopLaunchParameters | None = None - tunnel: RunloopTunnelConfig | None = None - gateways: dict[str, RunloopGatewaySpec] = Field(default_factory=dict) - mcp: dict[str, RunloopMcpSpec] = Field(default_factory=dict) - metadata: dict[str, str] = Field(default_factory=dict) - secret_refs: dict[str, str] = Field(default_factory=dict) - - -@dataclass(frozen=True) -class RunloopPlatformBlueprintsClient: - _sdk: Any - - async def list(self, **params: object) -> object: - return await self._sdk.blueprint.list(**params) - - async def list_public(self, **params: object) -> object: - return await self._sdk.api.blueprints.list_public(**params) - - def get(self, blueprint_id: str) -> Any: - return self._sdk.blueprint.from_id(blueprint_id) - - async def logs(self, blueprint_id: str, **params: object) -> object: - return await self._sdk.api.blueprints.logs(blueprint_id, **params) - - async def create(self, **params: object) -> object: - return await self._sdk.blueprint.create(**params) - - async def await_build_complete(self, blueprint_id: str, **params: object) -> object: - return await self._sdk.api.blueprints.await_build_complete(blueprint_id, **params) - - async def delete(self, blueprint_id: str, **params: object) -> object: - return await self.get(blueprint_id).delete(**params) - - -@dataclass(frozen=True) -class RunloopPlatformBenchmarksClient: - _sdk: Any - - async def list(self, **params: object) -> object: - return await self._sdk.benchmark.list(**params) - - async def list_public(self, **params: object) -> object: - return await self._sdk.api.benchmarks.list_public(**params) - - def get(self, benchmark_id: str) -> Any: - return self._sdk.benchmark.from_id(benchmark_id) - - async def create(self, **params: object) -> object: - return await self._sdk.benchmark.create(**params) - - async def update(self, benchmark_id: str, **params: object) -> object: - return await self.get(benchmark_id).update(**params) - - async def definitions(self, benchmark_id: str, **params: object) -> object: - return await self._sdk.api.benchmarks.definitions(benchmark_id, **params) - - async def start_run(self, benchmark_id: str, **params: object) -> object: - return await self.get(benchmark_id).start_run(**params) - - async def update_scenarios( - self, - benchmark_id: str, - *, - scenarios_to_add: tuple[str, ...] | Sequence[str] | None = None, - scenarios_to_remove: tuple[str, ...] | Sequence[str] | None = None, - **params: object, - ) -> object: - return await self._sdk.api.benchmarks.update_scenarios( - benchmark_id, - scenarios_to_add=scenarios_to_add, - scenarios_to_remove=scenarios_to_remove, - **params, - ) - - -@dataclass(frozen=True) -class RunloopPlatformSecretsClient: - _sdk: Any - - async def create(self, *, name: str, value: str, **params: object) -> object: - return await self._sdk.secret.create(name=name, value=value, **params) - - async def list(self, **params: object) -> object: - return await self._sdk.secret.list(**params) - - async def get(self, name: str, **params: object) -> object: - return await self._sdk.api.secrets.retrieve(name, **params) - - async def update(self, *, name: str, value: str, **params: object) -> object: - return await self._sdk.secret.update(name, value=value, **params) - - async def delete(self, name: str, **params: object) -> object: - return await self._sdk.secret.delete(name, **params) - - -@dataclass(frozen=True) -class RunloopPlatformNetworkPoliciesClient: - _sdk: Any - - async def create(self, **params: object) -> object: - return await self._sdk.network_policy.create(**params) - - async def list(self, **params: object) -> object: - return await self._sdk.network_policy.list(**params) - - def get(self, network_policy_id: str) -> Any: - return self._sdk.network_policy.from_id(network_policy_id) - - async def update(self, network_policy_id: str, **params: object) -> object: - return await self.get(network_policy_id).update(**params) - - async def delete(self, network_policy_id: str, **params: object) -> object: - return await self.get(network_policy_id).delete(**params) - - -@dataclass(frozen=True) -class RunloopPlatformAxonsClient: - _sdk: Any - - async def create(self, **params: object) -> object: - return await self._sdk.axon.create(**params) - - async def list(self, **params: object) -> object: - return await self._sdk.axon.list(**params) - - def get(self, axon_id: str) -> Any: - return self._sdk.axon.from_id(axon_id) - - async def publish(self, axon_id: str, **params: object) -> object: - return await self.get(axon_id).publish(**params) - - async def query_sql(self, axon_id: str, **params: object) -> object: - return await self.get(axon_id).sql.query(**params) - - async def batch_sql(self, axon_id: str, **params: object) -> object: - return await self.get(axon_id).sql.batch(**params) - - -@dataclass(frozen=True) -class RunloopPlatformClient: - """Thin facade over the Runloop SDK's non-devbox platform resources.""" - - _sdk: Any - - @property - def blueprints(self) -> RunloopPlatformBlueprintsClient: - return RunloopPlatformBlueprintsClient(self._sdk) - - @property - def benchmarks(self) -> RunloopPlatformBenchmarksClient: - return RunloopPlatformBenchmarksClient(self._sdk) - - @property - def secrets(self) -> RunloopPlatformSecretsClient: - return RunloopPlatformSecretsClient(self._sdk) - - @property - def network_policies(self) -> RunloopPlatformNetworkPoliciesClient: - return RunloopPlatformNetworkPoliciesClient(self._sdk) - - @property - def axons(self) -> RunloopPlatformAxonsClient: - return RunloopPlatformAxonsClient(self._sdk) - - -class RunloopSandboxSession(BaseSandboxSession): - """Runloop-backed sandbox session implementation.""" - - state: RunloopSandboxSessionState - _sdk: Any - _devbox: Any - _skip_start: bool - - def __init__(self, *, state: RunloopSandboxSessionState, sdk: Any, devbox: Any) -> None: - self.state = state - self._sdk = sdk - self._devbox = devbox - self._skip_start = False - - @classmethod - def from_state( - cls, - state: RunloopSandboxSessionState, - *, - sdk: Any, - devbox: Any, - ) -> RunloopSandboxSession: - return cls(state=state, sdk=sdk, devbox=devbox) - - @property - def devbox_id(self) -> str: - return self.state.devbox_id - - @property - def runloop_home(self) -> PurePosixPath: - return _effective_runloop_home(self.state.user_parameters) - - async def _resolved_envs(self) -> dict[str, str]: - manifest_envs = await self.state.manifest.environment.resolve() - return {**self.state.base_env_vars, **manifest_envs} - - def _coerce_exec_timeout(self, timeout_s: float | None) -> float: - if timeout_s is None: - return float(self.state.timeouts.exec_timeout_unbounded_s) - if timeout_s <= 0: - return 0.001 - return float(timeout_s) - - @redact_mount_error_data - async def start(self) -> None: - """Resume a reconnected Runloop devbox without replaying full setup when possible. - - `resume()` marks `_skip_start` when it successfully reconnects to a suspended devbox. - In that path, Runloop reuses the live machine and only reapplies snapshot or ephemeral - manifest state if the cached workspace fingerprint no longer matches. - """ - await self._validate_manifest_application() - if self._skip_start: - if await self.state.snapshot.restorable(dependencies=self.dependencies): - is_running = await self.running() - fingerprints_match = await self._can_skip_snapshot_restore_on_resume( - is_running=is_running - ) - if fingerprints_match: - await self._reapply_ephemeral_manifest_on_resume() - else: - await self._restore_snapshot_into_workspace_on_resume() - if self.should_provision_manifest_accounts_on_resume(): - await self.provision_manifest_accounts() - await self._reapply_ephemeral_manifest_on_resume() - else: - await self._reapply_ephemeral_manifest_on_resume() - return - await super().start() - - async def shutdown(self) -> None: - """Suspend or delete the underlying Runloop devbox as the final session cleanup step. - - `pause_on_exit=True` maps to Runloop suspension so the same devbox can be resumed later. - Otherwise the session shuts the devbox down and treats it as disposable. - """ - try: - if self.state.pause_on_exit: - await self._devbox.suspend(timeout=self.state.timeouts.suspend_s) - await self._devbox.await_suspended() - else: - await self._devbox.shutdown(timeout=self.state.timeouts.cleanup_s) - except Exception: - pass - - def supports_pty(self) -> bool: - return False - - async def _validate_path_access(self, path: Path | str, *, for_write: bool = False) -> Path: - return await self._validate_remote_path_access(path, for_write=for_write) - - def _runtime_helpers(self) -> tuple[RuntimeHelperScript, ...]: - return (RESOLVE_WORKSPACE_PATH_HELPER,) - - async def _wrap_command_in_workspace_context(self, command: str) -> str: - root_q = shlex.quote(self.state.manifest.root) - envs = await self._resolved_envs() - if not envs: - return f"cd {root_q} && {command}" - - env_assignments = " ".join( - shlex.quote(f"{key}={value}") for key, value in sorted(envs.items()) - ) - return f"cd {root_q} && env -- {env_assignments} {command}" - - async def _exec_internal( - self, - *command: str | Path, - timeout: float | None = None, - ) -> ExecResult: - cmd_str = await self._wrap_command_in_workspace_context(shlex.join(str(c) for c in command)) - return await self._run_exec_command( - cmd_str, - command=command, - timeout=timeout, - ) - - async def _run_exec_command( - self, - cmd_str: str, - *, - command: tuple[str | Path, ...], - timeout: float | None, - ) -> ExecResult: - caller_timeout = self._coerce_exec_timeout(timeout) - request_timeout = min(caller_timeout, self.state.timeouts.fast_op_s) - polling_config = _runloop_polling_config(timeout_s=caller_timeout) - - try: - result: RunloopAsyncExecutionResult = await asyncio.wait_for( - self._devbox.cmd.exec( - cmd_str, - timeout=request_timeout, - polling_config=polling_config, - ), - timeout=caller_timeout, - ) - stdout = (await result.stdout()).encode("utf-8", errors="replace") - stderr = (await result.stderr()).encode("utf-8", errors="replace") - exit_code = int(result.exit_code or 0) - return ExecResult(stdout=stdout, stderr=stderr, exit_code=exit_code) - except asyncio.TimeoutError as e: - raise ExecTimeoutError( - command=command, - timeout_s=timeout, - context=_runloop_error_context(e, backend_detail="exec_timeout"), - cause=e, - ) from e - except Exception as e: - if _is_runloop_timeout(e): - raise ExecTimeoutError( - command=command, - timeout_s=timeout, - context=_runloop_error_context(e, backend_detail="exec_timeout"), - cause=e, - ) from e - if _is_runloop_provider_error(e): - raise ExecTransportError( - command=command, - context=_runloop_error_context(e, backend_detail="exec_failed"), - cause=e, - retryable=_runloop_provider_retryability(e), - ) from e - raise ExecTransportError(command=command, cause=e) from e - - async def _ensure_tunnel_url(self, port: int) -> str: - try: - url = await self._devbox.get_tunnel_url(port, timeout=self.state.timeouts.fast_op_s) - except Exception as e: - if _is_runloop_provider_error(e): - raise ExposedPortUnavailableError( - port=port, - exposed_ports=self.state.exposed_ports, - reason="backend_unavailable", - context=_runloop_error_context(e, backend_detail="get_tunnel_url_failed"), - cause=e, - retryable=_runloop_provider_retryability(e), - ) from e - raise - if isinstance(url, str) and url: - return url - - try: - await self._devbox.net.enable_tunnel( - auth_mode="open", - http_keep_alive=True, - wake_on_http=False, - timeout=self.state.timeouts.fast_op_s, - ) - except Exception as e: - if _is_runloop_provider_error(e): - raise ExposedPortUnavailableError( - port=port, - exposed_ports=self.state.exposed_ports, - reason="backend_unavailable", - context=_runloop_error_context(e, backend_detail="enable_tunnel_failed"), - cause=e, - retryable=_runloop_provider_retryability(e), - ) from e - raise - try: - url = await self._devbox.get_tunnel_url(port, timeout=self.state.timeouts.fast_op_s) - except Exception as e: - if _is_runloop_provider_error(e): - context = _runloop_error_context(e, backend_detail="get_tunnel_url_failed") - context["phase"] = "post_enable" - raise ExposedPortUnavailableError( - port=port, - exposed_ports=self.state.exposed_ports, - reason="backend_unavailable", - context=context, - cause=e, - retryable=_runloop_provider_retryability(e), - ) from e - raise - if not isinstance(url, str) or not url: - raise ExposedPortUnavailableError( - port=port, - exposed_ports=self.state.exposed_ports, - reason="backend_unavailable", - context={"backend": "runloop", "detail": "missing_tunnel_url"}, - ) - return url - - async def resolve_exposed_port(self, port: int) -> ExposedPortEndpoint: - """Resolve an exposed Runloop port through the provider-managed tunnel endpoint. - - Runloop may not have a tunnel enabled for a devbox yet, so exposed-port resolution can - trigger tunnel creation before returning the public host, port, and TLS settings. - """ - - return await super().resolve_exposed_port(port) - - async def _resolve_exposed_port(self, port: int) -> ExposedPortEndpoint: - try: - url = await self._ensure_tunnel_url(port) - split = urlsplit(url) - host = split.hostname - if host is None: - raise ValueError("missing hostname") - port_value = split.port or (443 if split.scheme == "https" else 80) - return ExposedPortEndpoint(host=host, port=port_value, tls=split.scheme == "https") - except ExposedPortUnavailableError: - raise - except Exception as e: - raise ExposedPortUnavailableError( - port=port, - exposed_ports=self.state.exposed_ports, - reason="backend_unavailable", - context={"backend": "runloop", "detail": "invalid_tunnel_url"}, - cause=e, - ) from e - - async def _read_bounded(self, path: Path, *, max_bytes: int) -> bytes: - normalized_path = await self._validate_path_access(path) - try: - async with self._sdk.api.devboxes.with_streaming_response.download_file( - self.devbox_id, - path=sandbox_path_str(normalized_path), - timeout=self.state.timeouts.file_download_s, - ) as response: - return await collect_bounded(response.iter_bytes(chunk_size=65536), max_bytes) - except Exception as error: - if _is_runloop_not_found(error): - raise WorkspaceReadNotFoundError(path=path) from None - if _is_runloop_provider_error(error): - raise WorkspaceArchiveReadError( - path=path, retryable=_runloop_provider_retryability(error) - ) from None - raise - - async def read(self, path: Path | str, *, user: str | User | None = None) -> io.IOBase: - """Read a file via Runloop's binary file API.""" - error_path = posix_path_as_path(coerce_posix_path(path)) - if user is not None: - await self._check_read_with_exec(path, user=user) - - normalized_path = await self._validate_path_access(path) - try: - payload = await self._devbox.file.download( - path=sandbox_path_str(normalized_path), - timeout=self.state.timeouts.file_download_s, - ) - return io.BytesIO(bytes(payload)) - except Exception as e: - if _is_runloop_not_found(e): - raise WorkspaceReadNotFoundError( - path=error_path, - context=_runloop_error_context(e, backend_detail="file_download_failed"), - cause=e, - ) from e - if _is_runloop_provider_error(e): - raise WorkspaceArchiveReadError( - path=error_path, - context=_runloop_error_context(e, backend_detail="file_download_failed"), - cause=e, - retryable=_runloop_provider_retryability(e), - ) from e - raise WorkspaceArchiveReadError(path=error_path, cause=e) from e - - async def write( - self, - path: Path | str, - data: io.IOBase, - *, - user: str | User | None = None, - ) -> None: - """Write a file through Runloop's upload API using manifest-root workspace paths.""" - error_path = posix_path_as_path(coerce_posix_path(path)) - if user is not None: - await self._check_write_with_exec(path, user=user) - - payload = data.read() - if isinstance(payload, str): - payload = payload.encode("utf-8") - if not isinstance(payload, bytes | bytearray): - raise WorkspaceWriteTypeError(path=error_path, actual_type=type(payload).__name__) - - workspace_path = await self._validate_path_access(path, for_write=True) - await self.mkdir(workspace_path.parent, parents=True) - try: - await self._devbox.file.upload( - path=sandbox_path_str(workspace_path), - file=bytes(payload), - timeout=self.state.timeouts.file_upload_s, - ) - except Exception as e: - if _is_runloop_provider_error(e): - raise WorkspaceArchiveWriteError( - path=workspace_path, - context=_runloop_error_context(e, backend_detail="file_upload_failed"), - cause=e, - retryable=_runloop_provider_retryability(e), - ) from e - raise WorkspaceArchiveWriteError(path=workspace_path, cause=e) from e - - async def running(self) -> bool: - """Report whether the current Runloop devbox is still in the `running` backend state. - - Resume logic relies on this backend status check before deciding whether a suspended devbox - can be reused directly or whether snapshot restore must rebuild the workspace elsewhere. - """ - try: - info: RunloopDevboxView = await self._devbox.get_info( - timeout=self.state.timeouts.keepalive_s - ) - return cast(str, info.status) == "running" - except Exception: - return False - - async def mkdir( - self, - path: Path | str, - *, - parents: bool = False, - user: str | User | None = None, - ) -> None: - """Create directories via raw exec so workspace-root creation does not depend on `cd`.""" - - if user is not None: - path = await self._check_mkdir_with_exec(path, parents=parents, user=user) - else: - path = await self._validate_path_access(path, for_write=True) - cmd = ["mkdir"] - if parents: - cmd.append("-p") - cmd.extend(["--", sandbox_path_str(path)]) - result = await self._run_exec_command( - shlex.join(cmd), - command=tuple(cmd), - timeout=self.state.timeouts.fast_op_s, - ) - if not result.ok(): - raise WorkspaceArchiveWriteError( - path=path, - context={ - "reason": "mkdir_failed", - "exit_code": result.exit_code, - "stderr": result.stderr.decode("utf-8", "replace"), - }, - ) - - async def _backup_plain_skip_paths(self, plain_skip: set[Path]) -> bytes | None: - if not plain_skip: - return None - - root = sandbox_path_str(self.state.manifest.root) - root_q = shlex.quote(root) - checks = "\n".join( - ( - f"if [ -e {shlex.quote(rel.as_posix())} ]; then " - f'set -- "$@" {shlex.quote(rel.as_posix())}; fi' - ) - for rel in sorted(plain_skip, key=lambda p: p.as_posix()) - ) - command = ( - f"cd {root_q}\n" - "set --\n" - f"{checks}\n" - 'if [ "$#" -eq 0 ]; then exit 0; fi\n' - 'tar -cf - "$@" | base64 -w0\n' - ) - result = await self.exec(command, shell=True, timeout=self.state.timeouts.snapshot_s) - if not result.ok(): - raise WorkspaceArchiveReadError( - path=self._workspace_root_path(), - context={ - "reason": "ephemeral_backup_failed", - "exit_code": result.exit_code, - "stderr": result.stderr.decode("utf-8", "replace"), - }, - ) - encoded = result.stdout.decode("utf-8", "replace").strip() - if not encoded: - return None - try: - return io.BytesIO(base64.b64decode(encoded.encode("utf-8"), validate=True)).read() - except Exception as e: - raise WorkspaceArchiveReadError( - path=self._workspace_root_path(), - context={"reason": "ephemeral_backup_invalid_base64"}, - cause=e, - ) from e - - async def _remove_plain_skip_paths(self, plain_skip: set[Path]) -> None: - if not plain_skip: - return - root = self._workspace_root_path() - command = ["rm", "-rf", "--"] + [(root / rel).as_posix() for rel in sorted(plain_skip)] - result = await self.exec(*command, shell=False, timeout=self.state.timeouts.cleanup_s) - if not result.ok(): - raise WorkspaceArchiveReadError( - path=root, - context={ - "reason": "ephemeral_remove_failed", - "exit_code": result.exit_code, - "stderr": result.stderr.decode("utf-8", "replace"), - }, - ) - - async def _restore_plain_skip_paths(self, backup: bytes | None) -> None: - if not backup: - return - root = self._workspace_root_path() - temp_path = root / f".sandbox-runloop-restore-{self.state.session_id.hex}.tar" - await self.write(temp_path, io.BytesIO(backup)) - try: - result = await self.exec( - "mkdir", - "-p", - root.as_posix(), - shell=False, - timeout=self.state.timeouts.cleanup_s, - ) - if not result.ok(): - raise WorkspaceArchiveReadError( - path=root, - context={ - "reason": "ephemeral_restore_mkdir_failed", - "exit_code": result.exit_code, - }, - ) - result = await self.exec( - "tar", - "-xf", - sandbox_path_str(temp_path), - "-C", - root.as_posix(), - shell=False, - timeout=self.state.timeouts.snapshot_s, - ) - if not result.ok(): - raise WorkspaceArchiveReadError( - path=root, - context={ - "reason": "ephemeral_restore_failed", - "exit_code": result.exit_code, - "stderr": result.stderr.decode("utf-8", "replace"), - }, - ) - finally: - try: - await self.exec("rm", "-f", "--", sandbox_path_str(temp_path), shell=False) - except Exception: - pass - - async def persist_workspace(self) -> io.IOBase: - """Persist the workspace with a native Runloop disk snapshot. - - Before snapshotting, the session temporarily removes ephemeral skip paths and tears down - ephemeral mounts so the saved disk image contains only durable workspace state, then it - restores those local-only artifacts afterward. - """ - root = self._workspace_root_path() - skip = self._persist_workspace_skip_relpaths() - mount_targets = self.state.manifest.ephemeral_mount_targets() - mount_skip_rel_paths: set[Path] = set() - for _mount_entry, mount_path in mount_targets: - try: - mount_skip_rel_paths.add(mount_path.relative_to(root)) - except ValueError: - continue - plain_skip = skip - mount_skip_rel_paths - - backup: bytes | None = None - unmounted_mounts: list[tuple[Mount, Path]] = [] - snapshot_error: WorkspaceArchiveReadError | None = None - snapshot_id: str | None = None - - try: - backup = await self._backup_plain_skip_paths(plain_skip) - await self._remove_plain_skip_paths(plain_skip) - - for mount_entry, mount_path in mount_targets: - await mount_entry.mount_strategy.teardown_for_snapshot( - mount_entry, - self, - mount_path, - ) - unmounted_mounts.append((mount_entry, mount_path)) - - snapshot: RunloopAsyncSnapshot = await self._devbox.snapshot_disk( - name=f"sandbox-{self.state.session_id.hex[:12]}", - metadata={"openai_agents_session_id": self.state.session_id.hex}, - timeout=self.state.timeouts.snapshot_s, - ) - snapshot_id = snapshot.id - if not snapshot_id: - raise WorkspaceArchiveReadError( - path=root, - context={ - "reason": "snapshot_unexpected_return", - "type": type(snapshot).__name__, - }, - ) - except WorkspaceArchiveReadError as e: - snapshot_error = e - except Exception as e: - retryable = None - if _is_runloop_provider_error(e): - retryable = _runloop_provider_retryability(e) - snapshot_error = WorkspaceArchiveReadError( - path=root, - context={"reason": "snapshot_failed"}, - cause=e, - retryable=retryable, - ) - finally: - remount_error: WorkspaceArchiveReadError | None = None - for mount_entry, mount_path in reversed(unmounted_mounts): - try: - await mount_entry.mount_strategy.restore_after_snapshot( - mount_entry, self, mount_path - ) - except Exception as e: - current_error = WorkspaceArchiveReadError(path=root, cause=e) - if remount_error is None: - remount_error = current_error - else: - additional = remount_error.context.setdefault( - "additional_remount_errors", [] - ) - assert isinstance(additional, list) - additional.append( - { - "message": current_error.message, - "cause_type": type(e).__name__, - "cause": str(e), - } - ) - try: - await self._restore_plain_skip_paths(backup) - except Exception as e: - restore_error = WorkspaceArchiveReadError(path=root, cause=e) - if remount_error is None: - remount_error = restore_error - else: - additional = remount_error.context.setdefault("additional_restore_errors", []) - assert isinstance(additional, list) - additional.append( - { - "message": restore_error.message, - "cause_type": type(e).__name__, - "cause": str(e), - } - ) - - if remount_error is not None: - if snapshot_error is not None: - remount_error.context["snapshot_error_before_restore_corruption"] = { - "message": snapshot_error.message - } - raise remount_error - - if snapshot_error is not None: - raise snapshot_error - - assert snapshot_id is not None - return io.BytesIO(_encode_runloop_snapshot_ref(snapshot_id=snapshot_id)) - - async def hydrate_workspace(self, data: io.IOBase) -> None: - """Replace the current devbox from a Runloop snapshot reference or tar archive. - - Runloop restore creates a new devbox from the saved disk snapshot and treats that snapshot - filesystem as authoritative, including any tools or files that originally came from the - source blueprint, so restore does not reselect a blueprint. Non-native payloads fall back - to tar hydration so cross-provider snapshots and file snapshots keep working. - """ - root = self._workspace_root_path() - raw = data.read() - if isinstance(raw, str): - raw = raw.encode("utf-8") - if not isinstance(raw, bytes | bytearray): - raise WorkspaceWriteTypeError(path=root, actual_type=type(raw).__name__) - - snapshot_id = _decode_runloop_snapshot_ref(bytes(raw)) - if snapshot_id is None: - await self._hydrate_workspace_via_tar(bytes(raw)) - return - - try: - try: - await self._devbox.shutdown(timeout=self.state.timeouts.cleanup_s) - except Exception: - pass - envs = await self._resolved_envs() - create_kwargs = _runloop_create_kwargs( - blueprint_id=None, - blueprint_name=None, - env_vars=envs, - name=self.state.name, - user_parameters=self.state.user_parameters, - launch_parameters=self.state.launch_parameters, - tunnel=self.state.tunnel, - gateways=self.state.gateways, - mcp=self.state.mcp, - metadata=self.state.metadata, - secrets=self.state.secret_refs, - ) - devbox = await self._sdk.devbox.create_from_snapshot( - snapshot_id, - timeout=self.state.timeouts.resume_s, - **create_kwargs, - ) - self._devbox = devbox - self.state.devbox_id = devbox.id - except Exception as e: - context: dict[str, object] = { - "reason": "snapshot_restore_failed", - "snapshot_id": snapshot_id, - } - if _is_runloop_provider_error(e): - context.update(_runloop_error_context(e, backend_detail="snapshot_restore_failed")) - raise WorkspaceArchiveWriteError( - path=root, - context=context, - cause=e, - retryable=_runloop_provider_retryability(e) - if _is_runloop_provider_error(e) - else None, - ) from e - - async def _restore_snapshot_into_workspace_on_resume(self) -> None: - """Restore snapshots on resume, preserving Runloop's native disk-snapshot fast path.""" - - root = self._workspace_root_path() - workspace_archive = await self.state.snapshot.restore(dependencies=self.dependencies) - try: - raw = workspace_archive.read() - if isinstance(raw, str): - raw = raw.encode("utf-8") - if not isinstance(raw, bytes | bytearray): - raise WorkspaceWriteTypeError(path=root, actual_type=type(raw).__name__) - - payload = bytes(raw) - if _decode_runloop_snapshot_ref(payload) is None: - # Most providers restore tar snapshots by clearing the workspace first, then - # extracting into an empty root. Runloop differs only for its native snapshot - # refs, which already replace the entire devbox disk and therefore should not - # pre-clear the workspace root on resume. - await self._clear_workspace_root_on_resume() - await self.hydrate_workspace(io.BytesIO(payload)) - finally: - try: - workspace_archive.close() - except Exception: - pass - - async def _hydrate_workspace_via_tar(self, payload: bytes) -> None: - root = self._workspace_root_path() - archive_path = root / f".sandbox-runloop-hydrate-{self.state.session_id.hex}.tar" - - try: - validate_tar_bytes( - payload, - allow_external_symlink_targets=False, - ) - except UnsafeTarMemberError as e: - raise WorkspaceArchiveWriteError( - path=root, - context={ - "reason": "unsafe_or_invalid_tar", - "member": e.member, - "detail": str(e), - }, - cause=e, - ) from e - - try: - await self.mkdir(root, parents=True) - await self.write(archive_path, io.BytesIO(payload)) - result = await self.exec( - "tar", - "-C", - root.as_posix(), - "-xf", - archive_path.as_posix(), - shell=False, - timeout=self.state.timeouts.snapshot_s, - ) - if not result.ok(): - raise WorkspaceArchiveWriteError( - path=root, - context={ - "reason": "tar_extract_failed", - "exit_code": result.exit_code, - "stderr": result.stderr.decode("utf-8", errors="replace"), - }, - ) - except WorkspaceArchiveWriteError: - raise - except Exception as e: - raise WorkspaceArchiveWriteError(path=root, cause=e) from e - finally: - try: - await self.exec( - "rm", - "-f", - "--", - archive_path.as_posix(), - shell=False, - timeout=self.state.timeouts.cleanup_s, - ) - except Exception: - pass - - -def _runloop_create_kwargs( - *, - blueprint_id: str | None, - blueprint_name: str | None, - env_vars: dict[str, str] | None, - name: str | None, - user_parameters: RunloopUserParameters | None, - launch_parameters: RunloopLaunchParameters | None, - tunnel: RunloopTunnelConfig | None, - gateways: dict[str, RunloopGatewaySpec], - mcp: dict[str, RunloopMcpSpec], - metadata: dict[str, str], - secrets: dict[str, str], -) -> dict[str, object]: - kwargs: dict[str, object] = {} - if blueprint_id is not None: - kwargs["blueprint_id"] = blueprint_id - if blueprint_name is not None: - kwargs["blueprint_name"] = blueprint_name - if env_vars: - kwargs["environment_variables"] = env_vars - if name: - kwargs["name"] = name - launch_parameters_payload = _runloop_launch_parameters_payload( - launch_parameters=launch_parameters, - user_parameters=user_parameters, - ) - if launch_parameters_payload is not None: - kwargs["launch_parameters"] = launch_parameters_payload - if tunnel is not None: - kwargs["tunnel"] = tunnel.model_dump(mode="json", exclude_none=True) - if gateways: - kwargs["gateways"] = { - key: value.model_dump(mode="json", exclude_none=True) for key, value in gateways.items() - } - if mcp: - kwargs["mcp"] = { - key: value.model_dump(mode="json", exclude_none=True) for key, value in mcp.items() - } - if metadata: - kwargs["metadata"] = metadata - if secrets: - kwargs["secrets"] = secrets - return kwargs - - -def _runloop_launch_parameters_payload( - *, - launch_parameters: RunloopLaunchParameters | None, - user_parameters: RunloopUserParameters | None, -) -> dict[str, object] | None: - payload = ( - launch_parameters.to_dict(mode="json", exclude_none=True, exclude_defaults=True) - if launch_parameters is not None - else {} - ) - if user_parameters is not None: - payload["user_parameters"] = user_parameters.to_dict(mode="json", exclude_none=True) - return payload or None - - -async def _resolve_runloop_managed_secret_refs( - sdk: Any, - *, - managed_secrets: dict[str, str | RunloopExistingSecret] | None, - timeout_s: float, -) -> dict[str, str]: - if not managed_secrets: - return {} - - secret_refs: dict[str, str] = {} - for env_var, secret_value in sorted(managed_secrets.items()): - if not isinstance(secret_value, RunloopExistingSecret): - try: - await sdk.secret.create(name=env_var, value=secret_value, timeout=timeout_s) - except Exception as e: - if _is_runloop_conflict(e): - await sdk.secret.update(env_var, value=secret_value, timeout=timeout_s) - else: - raise - secret_refs[env_var] = env_var - return secret_refs - - -def _effective_runloop_home(user_parameters: RunloopUserParameters | None) -> PurePosixPath: - if user_parameters is None: - return _RUNLOOP_DEFAULT_HOME - if user_parameters.username == "root" and user_parameters.uid == 0: - return _RUNLOOP_ROOT_HOME - return PurePosixPath("/home") / user_parameters.username - - -def _default_runloop_manifest_root(user_parameters: RunloopUserParameters | None) -> str: - return str(_effective_runloop_home(user_parameters)) - - -def _validate_runloop_manifest_root( - manifest: Manifest, *, user_parameters: RunloopUserParameters | None -) -> None: - root = PurePosixPath(posixpath.normpath(manifest.root)) - runloop_home = _effective_runloop_home(user_parameters) - try: - root.relative_to(runloop_home) - except ValueError as e: - raise ValueError( - "RunloopSandboxClient requires manifest.root to be the effective Runloop home " - f"({runloop_home}) or a subdirectory of it." - ) from e - - -class RunloopSandboxClient(BaseSandboxClient[RunloopSandboxClientOptions | None]): - """Runloop sandbox client managing devbox lifecycle via AsyncRunloopSDK.""" - - backend_id = "runloop" - supports_default_options = True - _instrumentation: Instrumentation - _platform: RunloopPlatformClient - - def __init__( - self, - *, - bearer_token: str | None = None, - base_url: str | None = None, - instrumentation: Instrumentation | None = None, - dependencies: Dependencies | None = None, - ) -> None: - self._sdk = _import_runloop_sdk().async_sdk(bearer_token=bearer_token, base_url=base_url) - self._platform = RunloopPlatformClient(self._sdk) - self._instrumentation = ( - instrumentation if instrumentation is not None else Instrumentation() - ) - self._dependencies = dependencies - - @property - def platform(self) -> RunloopPlatformClient: - return self._platform - - @redact_mount_error_data - async def create( - self, - *, - snapshot: SnapshotSpec | SnapshotBase | None = None, - manifest: Manifest | None = None, - options: RunloopSandboxClientOptions | None, - ) -> SandboxSession: - """Create a Runloop devbox and bind it to a manifest rooted under the active home. - - Runloop defaults to the `user` account at `/home/user`, but explicit user parameters can - switch the active home, including root launch at `/root`. Client creation validates the - manifest root against that effective home, merges environment variables, and applies any - configured blueprint selection or user profile when provisioning the devbox. The returned - session follows the shared sandbox lifecycle and must be started before direct operations. - """ - resolved_options = options if options is not None else RunloopSandboxClientOptions() - if ( - resolved_options.blueprint_id is not None - and resolved_options.blueprint_name is not None - ): - raise ValueError( - "RunloopSandboxClientOptions cannot set both blueprint_id and blueprint_name" - ) - - user_parameters = _normalize_runloop_user_parameters(resolved_options.user_parameters) - manifest = ( - manifest - if manifest is not None - else Manifest(root=_default_runloop_manifest_root(user_parameters)) - ) - _validate_runloop_manifest_root(manifest, user_parameters=user_parameters) - self._validate_manifest_for_create(manifest) - - timeouts_in = resolved_options.timeouts - if isinstance(timeouts_in, RunloopTimeouts): - timeouts = timeouts_in - elif timeouts_in is None: - timeouts = RunloopTimeouts() - else: - timeouts = RunloopTimeouts.model_validate(timeouts_in) - - secret_refs = await _resolve_runloop_managed_secret_refs( - self._sdk, - managed_secrets=resolved_options.managed_secrets, - timeout_s=timeouts.fast_op_s, - ) - launch_parameters = _normalize_runloop_launch_parameters(resolved_options.launch_parameters) - tunnel = _normalize_runloop_tunnel_config(resolved_options.tunnel) - base_envs = dict(resolved_options.env_vars or {}) - manifest_envs = await manifest.environment.resolve() - envs = {**base_envs, **manifest_envs} or None - - create_kwargs = _runloop_create_kwargs( - blueprint_id=resolved_options.blueprint_id, - blueprint_name=resolved_options.blueprint_name, - env_vars=envs, - name=resolved_options.name, - user_parameters=user_parameters, - launch_parameters=launch_parameters, - tunnel=tunnel, - gateways=dict(resolved_options.gateways or {}), - mcp=dict(resolved_options.mcp or {}), - metadata=dict(resolved_options.metadata or {}), - secrets=secret_refs, - ) - devbox = await self._sdk.devbox.create(timeout=timeouts.create_s, **create_kwargs) - - session_id = uuid.uuid4() - snapshot_instance = resolve_snapshot(snapshot, str(session_id)) - state = RunloopSandboxSessionState( - session_id=session_id, - manifest=manifest, - snapshot=snapshot_instance, - devbox_id=devbox.id, - blueprint_id=resolved_options.blueprint_id, - blueprint_name=resolved_options.blueprint_name, - base_env_vars=base_envs, - pause_on_exit=resolved_options.pause_on_exit, - name=resolved_options.name, - timeouts=timeouts, - exposed_ports=resolved_options.exposed_ports, - user_parameters=user_parameters, - launch_parameters=launch_parameters, - tunnel=tunnel, - gateways=dict(resolved_options.gateways or {}), - mcp=dict(resolved_options.mcp or {}), - metadata=dict(resolved_options.metadata or {}), - secret_refs=secret_refs, - ) - inner = RunloopSandboxSession.from_state(state, sdk=self._sdk, devbox=devbox) - return self._wrap_session(inner, instrumentation=self._instrumentation) - - async def close(self) -> None: - """Close the shared AsyncRunloopSDK client used for devbox operations.""" - await self._sdk.aclose() - - async def __aenter__(self) -> RunloopSandboxClient: - return self - - async def __aexit__(self, *_: object) -> None: - await self.close() - - async def delete(self, session: SandboxSession) -> SandboxSession: - """Best-effort release the Runloop devbox when callers delete the session.""" - inner = session._inner - if not isinstance(inner, RunloopSandboxSession): - raise TypeError("RunloopSandboxClient.delete expects a RunloopSandboxSession") - try: - await inner.shutdown() - except Exception: - pass - return session - - @redact_mount_error_data - async def resume( - self, - state: SandboxSessionState, - ) -> SandboxSession: - """Resume a persisted Runloop session by reconnecting or reprovisioning a devbox. - - The client first tries to reconnect to the stored devbox id, including after an unclean - process/client shutdown where the devbox is still running and `shutdown()` was never - called. If reconnect fails, it creates a fresh devbox with the stored blueprint and - environment settings. - """ - if not isinstance(state, RunloopSandboxSessionState): - raise TypeError("RunloopSandboxClient.resume expects a RunloopSandboxSessionState") - state.assert_path_grants_rebound() - - devbox = None - reconnected = False - try: - devbox = self._sdk.devbox.from_id(state.devbox_id) - info: RunloopDevboxView = await devbox.get_info(timeout=state.timeouts.keepalive_s) - status = info.status - resume_polling_config = _runloop_polling_config(timeout_s=state.timeouts.resume_s) - if status == "suspended": - await devbox.resume(timeout=state.timeouts.resume_s) - await devbox.await_running(polling_config=resume_polling_config) - elif status == "resuming": - await devbox.await_running(polling_config=resume_polling_config) - elif status != "running": - raise RuntimeError(f"unexpected_status:{status}") - reconnected = True - except Exception: - devbox = None - - if devbox is None: - manifest_envs = await state.manifest.environment.resolve() - envs = {**state.base_env_vars, **manifest_envs} or None - create_kwargs = _runloop_create_kwargs( - blueprint_id=state.blueprint_id, - blueprint_name=state.blueprint_name, - env_vars=envs, - name=state.name, - user_parameters=state.user_parameters, - launch_parameters=state.launch_parameters, - tunnel=state.tunnel, - gateways=state.gateways, - mcp=state.mcp, - metadata=state.metadata, - secrets=state.secret_refs, - ) - devbox = await self._sdk.devbox.create(timeout=state.timeouts.create_s, **create_kwargs) - state.devbox_id = devbox.id - - inner = RunloopSandboxSession.from_state(state, sdk=self._sdk, devbox=devbox) - inner._skip_start = state.pause_on_exit and reconnected - inner._set_start_state_preserved(reconnected, system=reconnected) - return self._wrap_session(inner, instrumentation=self._instrumentation) - - def deserialize_session_state(self, payload: dict[str, object]) -> SandboxSessionState: - return self._deserialize_session_state_payload(payload, RunloopSandboxSessionState) diff --git a/src/agents/handoffs/__init__.py b/src/agents/handoffs/__init__.py index 5081c588c5..0ab860c70c 100644 --- a/src/agents/handoffs/__init__.py +++ b/src/agents/handoffs/__init__.py @@ -34,6 +34,7 @@ if TYPE_CHECKING: from ..agent import Agent, AgentBase + from ..tool import ToolErrorFunction # The handoff input type is the type of data passed when the agent is called via a handoff. @@ -207,6 +208,19 @@ class Handoff(Generic[TContext, TAgent]): _default_tool_identity: tuple[str, str] | None = field(default=None, kw_only=True, repr=False) """The target agent name and derived tool name when the default was used.""" + failure_error_function: ToolErrorFunction | None = None + """Optional invocation-error formatter used by ``Runner`` (not Realtime sessions). + + ``None`` preserves exception propagation. A sync or async formatter returns the tool + output sent to the calling agent, which can then retry or answer without transferring. + For a generic message, pass ``agents.default_tool_error_function``. Custom formatters + must return only application-approved error details and may re-raise unrecoverable errors. + This also handles application ``on_invoke_handoff``/``on_handoff`` exceptions; recovery + does not roll back callback side effects. Cancellation and formatter errors propagate. + Transfer hooks and input filters run only after a successful invocation and are not + covered by this handler. + """ + def get_transfer_message(self, agent: AgentBase[Any]) -> str: return json.dumps({"assistant": agent.name}) @@ -234,6 +248,7 @@ def handoff( input_filter: Callable[[HandoffInputData], HandoffInputData] | None = None, nest_handoff_history: bool | None = None, is_enabled: bool | Callable[[RunContextWrapper[Any], Agent[Any]], MaybeAwaitable[bool]] = True, + failure_error_function: ToolErrorFunction | None = None, ) -> Handoff[TContext, Agent[TContext]]: ... @@ -248,6 +263,7 @@ def handoff( input_filter: Callable[[HandoffInputData], HandoffInputData] | None = None, nest_handoff_history: bool | None = None, is_enabled: bool | Callable[[RunContextWrapper[Any], Agent[Any]], MaybeAwaitable[bool]] = True, + failure_error_function: ToolErrorFunction | None = None, ) -> Handoff[TContext, Agent[TContext]]: ... @@ -261,6 +277,7 @@ def handoff( input_filter: Callable[[HandoffInputData], HandoffInputData] | None = None, nest_handoff_history: bool | None = None, is_enabled: bool | Callable[[RunContextWrapper[Any], Agent[Any]], MaybeAwaitable[bool]] = True, + failure_error_function: ToolErrorFunction | None = None, ) -> Handoff[TContext, Agent[TContext]]: ... @@ -274,6 +291,7 @@ def handoff( nest_handoff_history: bool | None = None, is_enabled: bool | Callable[[RunContextWrapper[Any], Agent[TContext]], MaybeAwaitable[bool]] = True, + failure_error_function: ToolErrorFunction | None = None, ) -> Handoff[TContext, Agent[TContext]]: """Create a handoff from an agent. @@ -295,6 +313,13 @@ def handoff( is_enabled: Whether the handoff is enabled. Can be a bool or a callable that takes the run context and agent and returns whether the handoff is enabled. Disabled handoffs are hidden from the LLM at runtime. + failure_error_function: Optional sync or async invocation-error formatter for Runner. + None preserves exception propagation. Pass ``default_tool_error_function`` for + generic model-visible feedback, or a custom formatter returning application-approved + details. The calling agent stays active after a handled failure. Application callback + errors are also handled, without rolling back side effects; the formatter may re-raise + errors that are unsafe to recover. Cancellation, formatter errors, and errors in + transfer hooks or input filters propagate. """ if input_type is not None and on_handoff is None: @@ -374,6 +399,7 @@ async def _is_enabled(ctx: RunContextWrapper[Any], agent_base: AgentBase[Any]) - input_filter=input_filter, nest_handoff_history=nest_handoff_history, agent_name=agent.name, + failure_error_function=failure_error_function, is_enabled=_is_enabled if callable(is_enabled) else is_enabled, _default_tool_identity=(agent.name, tool_name) if not tool_name_override else None, ) diff --git a/src/agents/handoffs/history.py b/src/agents/handoffs/history.py index b0ac856f65..221abc92e0 100644 --- a/src/agents/handoffs/history.py +++ b/src/agents/handoffs/history.py @@ -506,7 +506,8 @@ def _split_summary_records(body: str) -> list[str]: current: list[str] = [] current_is_numbered = False - for raw_line in body.splitlines(): + # The writer separates records with LF. Other line separators can be message content. + for raw_line in body.split("\n"): if not raw_line.strip(): continue diff --git a/src/agents/memory/__init__.py b/src/agents/memory/__init__.py index bb5c7356f7..f266ea73d7 100644 --- a/src/agents/memory/__init__.py +++ b/src/agents/memory/__init__.py @@ -9,9 +9,10 @@ OpenAIResponsesCompactionAwareSession, Session, SessionABC, + await_session_mutation, is_openai_responses_compaction_aware_session, ) -from .session_settings import SessionSettings +from .session_settings import SessionSettings, coerce_session_settings, resolve_session_limit from .util import SessionInputCallback if TYPE_CHECKING: @@ -22,6 +23,9 @@ "SessionABC", "SessionInputCallback", "SessionSettings", + "await_session_mutation", + "coerce_session_settings", + "resolve_session_limit", "SQLiteSession", "OpenAIConversationsSession", "OpenAIResponsesCompactionSession", diff --git a/src/agents/memory/session.py b/src/agents/memory/session.py index 909af12e63..59dd6cca27 100644 --- a/src/agents/memory/session.py +++ b/src/agents/memory/session.py @@ -18,8 +18,21 @@ _T = TypeVar("_T") -async def _await_mutation(awaitable: Awaitable[_T]) -> _T: - """Wait for a mutation outcome despite repeated caller cancellation.""" +async def await_session_mutation(awaitable: Awaitable[_T]) -> _T: + """Wait for a session mutation to settle before propagating caller cancellation. + + Once this coroutine starts, cancellation of its caller does not cancel the + supplied awaitable. Repeated cancellation requests are deferred until the + operation finishes. Without caller cancellation, return the result or raise + the operation's exception. With caller cancellation, retrieve the operation's + outcome and raise the first caller cancellation instead, even if the operation + failed. The caller must not interpret that cancellation as a rolled-back write. + + This helper does not provide a timeout, transaction, retry, or mutual exclusion. + A stuck operation can delay cancellation indefinitely; configure timeouts on + the underlying client. Independently cancelling the supplied operation can + still interrupt it. Cancellation before this coroutine starts is not covered. + """ task = asyncio.ensure_future(awaitable) cancellation: asyncio.CancelledError | None = None while not task.done(): @@ -40,6 +53,10 @@ async def _await_mutation(awaitable: Awaitable[_T]) -> _T: return result +# Preserve internal imports while sharing one implementation with adapter authors. +_await_mutation = await_session_mutation + + @dataclass(frozen=True) class _CompactionSnapshot: """Bounded logical history with a backend-owned atomic suffix replacement.""" diff --git a/src/agents/memory/session_settings.py b/src/agents/memory/session_settings.py index eb42f617f2..4da22505bc 100644 --- a/src/agents/memory/session_settings.py +++ b/src/agents/memory/session_settings.py @@ -19,7 +19,13 @@ def resolve_session_limit( explicit_limit: int | None, settings: SessionSettings | dict[str, Any] | None, ) -> int | None: - """Safely resolve the effective limit for session operations.""" + """Return an explicit limit, or fall back to the session settings' limit. + + ``None`` selects the default; if both are absent, return ``None`` (unlimited). + Zero and negative explicit limits are preserved. A session implementation + should return no items for those values. This helper selects a limit; it does + not slice or fetch history. Dictionary settings use :func:`coerce_session_settings`. + """ if explicit_limit is not None: return explicit_limit if settings is not None: @@ -67,7 +73,14 @@ def to_dict(self) -> dict[str, Any]: def coerce_session_settings( value: SessionSettings | dict[str, Any], ) -> SessionSettings: - """Normalize session settings while preserving existing typed instances.""" + """Normalize a settings object or dictionary to :class:`SessionSettings`. + + Existing settings instances (including subclasses) are returned unchanged. + Dictionary inputs reject unknown fields with ``TypeError``. Known fields use + the ``SessionSettings`` constructor's Pydantic validation; invalid field values + raise its validation errors. + Callers that accept ``None`` should create ``SessionSettings()`` for that case. + """ return _coerce_session_settings(value, settings_type=SessionSettings) diff --git a/src/agents/model_settings.py b/src/agents/model_settings.py index 1e3be034c7..bbd68d7485 100644 --- a/src/agents/model_settings.py +++ b/src/agents/model_settings.py @@ -251,6 +251,12 @@ def __init__( timeout: Annotated[FiniteFloat, Field(gt=0)] | None = None, ) -> None: ... + def __setattr__(self, name: str, value: Any) -> None: + object.__setattr__(self, name, value) + # SDK-owned defaults become explicit on assignment, even if the value is equal. + if "_agent_defaults_untouched" in self.__dict__ and name in self.__dataclass_fields__: + self.__dict__["_agent_defaults_untouched"] = False + def resolve(self, override: ModelSettings | dict[str, Any] | None) -> ModelSettings: """Produce a new ModelSettings by overlaying any non-None values from the override on top of this instance.""" diff --git a/src/agents/models/chatcmpl_converter.py b/src/agents/models/chatcmpl_converter.py index 2eff812424..6368783550 100644 --- a/src/agents/models/chatcmpl_converter.py +++ b/src/agents/models/chatcmpl_converter.py @@ -302,6 +302,9 @@ def maybe_easy_input_message(cls, item: Any) -> EasyInputMessageParam | None: if role not in ("user", "assistant", "system", "developer"): return None + if cls.maybe_response_output_message(item): + return None + return cast(EasyInputMessageParam, item) @classmethod @@ -349,12 +352,23 @@ def maybe_item_reference(cls, item: Any) -> ItemReference | None: @classmethod def maybe_response_output_message(cls, item: Any) -> ResponseOutputMessageParam | None: - # ResponseOutputMessage is only used for messages with role assistant + # Session replay strips storage IDs from assistant output messages. if ( isinstance(item, dict) and item.get("type") == "message" and item.get("role") == "assistant" - and {"id", "content"} <= set(item) + and "content" in item + and ( + "id" in item + or ( + isinstance(item["content"], list) + and len(item["content"]) > 0 + and all( + isinstance(part, dict) and part.get("type") in ("output_text", "refusal") + for part in item["content"] + ) + ) + ) ): return cast(ResponseOutputMessageParam, item) return None diff --git a/src/agents/models/interface.py b/src/agents/models/interface.py index 6c4bd2cc03..07cdcd4b35 100644 --- a/src/agents/models/interface.py +++ b/src/agents/models/interface.py @@ -44,6 +44,18 @@ class Model(abc.ABC): call ID for correlation. """ + def get_default_model_settings(self) -> ModelSettings | None: + """Return defaults for this model, or None to retain the agent's implicit defaults. + + The default implementation recognizes adapters exposing a string ``model`` name. + Adapters with namespaced identifiers can override this using their canonical model name. + Explicit agent settings and explicit model selections take precedence over this hook. + """ + from .default_models import get_default_model_settings + + name = getattr(self, "model", None) + return get_default_model_settings(name) if isinstance(name, str) else None + async def _cleanup_on_run_end(self, owner: object) -> None: """Release run-scoped resources after the runner finishes using this model.""" return None diff --git a/src/agents/models/openai_chatcompletions.py b/src/agents/models/openai_chatcompletions.py index 0006cf5747..64248ba9d8 100644 --- a/src/agents/models/openai_chatcompletions.py +++ b/src/agents/models/openai_chatcompletions.py @@ -680,6 +680,15 @@ async def _fetch_response( reasoning_effort = ( model_settings.reasoning.effort if model_settings.reasoning is not None else None ) + # These models require non-reasoning mode for Chat Completions function tools. + # Keep Responses defaults and caller-provided effort overrides unchanged. + if ( + reasoning_effort is None + and self.model in {"gpt-6-sol", "gpt-6-luna"} + and converted_tools + and "reasoning_effort" not in (model_settings.extra_args or {}) + ): + reasoning_effort = "none" store = ChatCmplHelpers.get_store_param(self._get_client(), model_settings) stream_options = ChatCmplHelpers.get_stream_options_param( diff --git a/src/agents/realtime/session.py b/src/agents/realtime/session.py index d9c502d866..60aca77ded 100644 --- a/src/agents/realtime/session.py +++ b/src/agents/realtime/session.py @@ -382,8 +382,8 @@ async def send_audio(self, audio: bytes, *, commit: bool = False) -> None: await self._model.send_event(RealtimeModelSendAudio(audio=audio, commit=commit)) async def interrupt(self) -> None: - """Interrupt the model.""" - await self._model.send_event(RealtimeModelSendInterrupt()) + """Interrupt model generation and audio playback without cancelling running tools.""" + await self._model.send_event(RealtimeModelSendInterrupt(force_response_cancel=True)) async def update_agent(self, agent: RealtimeAgent) -> None: """Update the active agent for this session and apply its settings to the model.""" diff --git a/src/agents/run_internal/agent_bindings.py b/src/agents/run_internal/agent_bindings.py index 93e3702b14..f868078401 100644 --- a/src/agents/run_internal/agent_bindings.py +++ b/src/agents/run_internal/agent_bindings.py @@ -4,6 +4,7 @@ from typing import Generic from ..agent import Agent +from ..model_settings import ModelSettings from ..run_context import TContext __all__ = [ @@ -19,6 +20,7 @@ class AgentBindings(Generic[TContext]): public_agent: Agent[TContext] execution_agent: Agent[TContext] + model_settings_overrides: ModelSettings | None = None def bind_public_agent(agent: Agent[TContext]) -> AgentBindings[TContext]: @@ -30,9 +32,11 @@ def bind_execution_agent( *, public_agent: Agent[TContext], execution_agent: Agent[TContext], + model_settings_overrides: ModelSettings | None = None, ) -> AgentBindings[TContext]: """Build bindings for execution-only clones such as sandbox-prepared agents.""" return AgentBindings( public_agent=public_agent, execution_agent=execution_agent, + model_settings_overrides=model_settings_overrides, ) diff --git a/src/agents/run_internal/run_loop.py b/src/agents/run_internal/run_loop.py index f7050bf679..b4df65bc2c 100644 --- a/src/agents/run_internal/run_loop.py +++ b/src/agents/run_internal/run_loop.py @@ -2513,7 +2513,9 @@ async def raise_if_input_guardrail_tripwire_known() -> None: model = get_model(execution_agent, run_config) tool_use_tracker.record_model(model) - model_settings = get_model_settings(execution_agent, run_config) + model_settings = get_model_settings( + public_agent, run_config, model, prepared_overrides=bindings.model_settings_overrides + ) model_settings = maybe_reset_tool_choice(public_agent, tool_use_tracker, model_settings) final_response: ModelResponse | None = None @@ -2645,10 +2647,21 @@ async def rewind_model_request() -> None: ), ) + stream_event_count = 0 + last_stream_event_type: str | None = None + stream_response_id: str | None = None + stream_request_id: str | None = None + # Raising out of this loop leaves the model stream suspended at its yield, so close it # explicitly instead of waiting for garbage collection to finalize the generator chain. async with aclosing(model_run_context_stream(retry_stream, tool_use_tracker)) as model_events: async for event in model_events: + stream_event_count += 1 + last_stream_event_type = getattr(event, "type", None) + event_response = getattr(event, "response", None) + if isinstance(event_response, Response): + stream_response_id = event_response.id + stream_request_id = getattr(event_response, "_request_id", None) streamed_result._event_queue.put_nowait(RawResponsesStreamEvent(data=event)) terminal_response: Response | None = None @@ -2702,7 +2715,13 @@ async def rewind_model_request() -> None: streamed_response_output.append(event.item) if final_response is None: - raise ModelBehaviorError("Model did not produce a final response!") + raise ModelBehaviorError( + "Model did not produce a final response! The stream ended before a " + "response.completed event was received. " + f"event_count={stream_event_count}, last_event_type={last_stream_event_type!r}, " + f"response_id={stream_response_id!r}, request_id={stream_request_id!r}. " + "Share these IDs when reporting this issue; None means no value was observed." + ) record_compaction_model_response( session, context_wrapper, compaction_input_digests, final_response, reasoning_item_id_policy @@ -2957,7 +2976,9 @@ async def get_new_response( model = get_model(execution_agent, run_config) tool_use_tracker.record_model(model) - model_settings = get_model_settings(execution_agent, run_config) + model_settings = get_model_settings( + public_agent, run_config, model, prepared_overrides=bindings.model_settings_overrides + ) model_settings = maybe_reset_tool_choice(public_agent, tool_use_tracker, model_settings) if server_conversation_tracker is not None: diff --git a/src/agents/run_internal/tool_actions.py b/src/agents/run_internal/tool_actions.py index 21a97af88b..18ae37b772 100644 --- a/src/agents/run_internal/tool_actions.py +++ b/src/agents/run_internal/tool_actions.py @@ -624,7 +624,7 @@ async def _run_call(span: Any | None) -> RunItem: log_tool_action_error("Shell executor failed", exc) raw_entries: list[dict[str, Any]] | None = None - if shell_output_payload: + if shell_output_payload is not None: raw_entries = shell_output_payload elif output_text: raw_entries = [ diff --git a/src/agents/run_internal/tool_execution.py b/src/agents/run_internal/tool_execution.py index 7f8e86ce54..d47e98db5d 100644 --- a/src/agents/run_internal/tool_execution.py +++ b/src/agents/run_internal/tool_execution.py @@ -2382,6 +2382,9 @@ async def run_native_tool_post_invoke( task = asyncio.ensure_future(post_invoke) task.add_done_callback(_consume_future_exception) try: + # Invocation error conversion may consume cancellation before finalization starts. + if sibling_category_failure.is_set(): + raise asyncio.CancelledError await asyncio.shield(task) except asyncio.CancelledError: if sibling_category_failure.is_set(): @@ -2417,6 +2420,8 @@ async def execute_custom_tool_calls( results: list[RunItem] = [] for call in calls: + if sibling_category_failure is not None and sibling_category_failure.is_set(): + break results.append( await CustomToolAction.execute( agent=public_agent, @@ -2446,6 +2451,8 @@ async def execute_local_shell_calls( results: list[RunItem] = [] for call in calls: + if sibling_category_failure is not None and sibling_category_failure.is_set(): + break results.append( await LocalShellAction.execute( agent=public_agent, @@ -2475,6 +2482,8 @@ async def execute_shell_calls( results: list[RunItem] = [] for call in calls: + if sibling_category_failure is not None and sibling_category_failure.is_set(): + break results.append( await ShellAction.execute( agent=public_agent, @@ -2504,6 +2513,8 @@ async def execute_apply_patch_calls( results: list[RunItem] = [] for call in calls: + if sibling_category_failure is not None and sibling_category_failure.is_set(): + break results.append( await ApplyPatchAction.execute( agent=public_agent, @@ -2533,6 +2544,8 @@ async def execute_computer_actions( results: list[RunItem] = [] for action in actions: + if sibling_category_failure is not None and sibling_category_failure.is_set(): + break context_wrapper._mark_tool_invocation_executed( action.tool_call, tool_name=action.computer_tool.name, diff --git a/src/agents/run_internal/tool_planning.py b/src/agents/run_internal/tool_planning.py index 4f247c92fc..b8134d8cef 100644 --- a/src/agents/run_internal/tool_planning.py +++ b/src/agents/run_internal/tool_planning.py @@ -5,6 +5,7 @@ import inspect import json from collections.abc import Awaitable, Callable, Hashable, Mapping, Sequence +from functools import partial from typing import Any, TypeVar, cast from openai.types.responses import ResponseFunctionToolCall @@ -41,6 +42,7 @@ from ..tool import FunctionTool, MCPToolApprovalRequest, get_function_tool_origin from ..tool_guardrails import ToolInputGuardrailResult, ToolOutputGuardrailResult from ..util._asyncio_tasks import gather_with_cancel +from . import tool_execution from .agent_bindings import AgentBindings from .run_steps import ( ProcessedResponse, @@ -61,6 +63,7 @@ execute_local_shell_calls, execute_shell_calls, get_mapping_or_attr, + log_tool_action_error, ) T = TypeVar("T") @@ -283,7 +286,11 @@ def _register_tool_call_items( continue if isinstance(item, ToolApprovalItem): continue - if not validate_invocations and isinstance(item, ToolCallItem | MCPApprovalRequestItem): + if ( + not validate_invocations + and context_wrapper._allow_legacy_approval_binding_reconstruction + and isinstance(item, ToolCallItem | MCPApprovalRequestItem) + ): raw_type = get_mapping_or_attr(item.raw_item, "type") tool_name = get_mapping_or_attr(item.raw_item, "name") if not isinstance(tool_name, str): @@ -352,44 +359,77 @@ def _dedupe_processed_response_invocations( for item in existing_items if (output_identity := tool_output_identity(getattr(item, "raw_item", None))) is not None } - completed_historical_invocations: dict[str, tuple[str, str, str]] = {} + # Fingerprinting every completed call in the history on every turn is quadratic in run + # length, so only call IDs that are reused in history or by this response are fingerprinted. + historical_entries: dict[str, list[tuple[str, str, str] | RunItem]] = {} for item in existing_items: - raw_item = getattr(item, "raw_item", None) - identity = tool_invocation_identity( - raw_item, - tool_lookup_key=(item.tool_lookup_key if isinstance(item, ToolApprovalItem) else None), - tool_name=( - item.tool_name if isinstance(item, ToolCallItem | ToolApprovalItem) else None - ), - invocation_role=("handoff" if isinstance(item, HandoffCallItem) else None), - ) + call_identity = tool_invocation_call_id(getattr(item, "raw_item", None)) + if ( + call_identity is None + or call_identity[1] is None + or call_identity not in completed_output_keys + ): + continue + entry: tuple[str, str, str] | RunItem = item if ( context_wrapper._allow_legacy_approval_binding_reconstruction and isinstance(item, ToolCallItem) and item.tool_name is None ): - call_identity = tool_invocation_call_id(raw_item) - if call_identity is not None and call_identity[1] is not None: - legacy_record = context_wrapper._tool_invocations.get(call_identity[1]) - if ( - legacy_record is not None - and legacy_record.completed - and legacy_record.invocation_type == call_identity[0] - ): - identity = ( - legacy_record.invocation_type, - call_identity[1], - legacy_record.fingerprint, - ) - if identity is None or identity[:2] not in completed_output_keys: + legacy_record = context_wrapper._tool_invocations.get(call_identity[1]) + if ( + legacy_record is not None + and legacy_record.completed + and legacy_record.invocation_type == call_identity[0] + ): + entry = ( + legacy_record.invocation_type, + call_identity[1], + legacy_record.fingerprint, + ) + historical_entries.setdefault(call_identity[1], []).append(entry) + + def historical_entry_identity( + entry: tuple[str, str, str] | RunItem, + ) -> tuple[str, str, str] | None: + if isinstance(entry, tuple): + return entry + return tool_invocation_identity( + entry.raw_item, + tool_lookup_key=( + entry.tool_lookup_key if isinstance(entry, ToolApprovalItem) else None + ), + tool_name=( + entry.tool_name if isinstance(entry, ToolCallItem | ToolApprovalItem) else None + ), + invocation_role=("handoff" if isinstance(entry, HandoffCallItem) else None), + ) + + completed_historical_invocations: dict[str, tuple[str, str, str] | None] = {} + for call_id, entries in historical_entries.items(): + if len(entries) < 2: continue - previous_identity = completed_historical_invocations.get(identity[1]) - if previous_identity is not None and previous_identity != identity: - raise ModelBehaviorError( - "Run history reused a tool call ID for different completed invocations. " - "Use a unique call ID for each tool invocation." + resolved_identity: tuple[str, str, str] | None = None + for entry in entries: + identity = historical_entry_identity(entry) + if identity is None: + continue + if resolved_identity is not None and resolved_identity != identity: + raise ModelBehaviorError( + "Run history reused a tool call ID for different completed invocations. " + "Use a unique call ID for each tool invocation." + ) + resolved_identity = identity + completed_historical_invocations[call_id] = resolved_identity + + def get_historical_identity(call_id: str) -> tuple[str, str, str] | None: + if call_id not in completed_historical_invocations: + entries = historical_entries.get(call_id) + completed_historical_invocations[call_id] = ( + historical_entry_identity(entries[0]) if entries else None ) - completed_historical_invocations[identity[1]] = identity + return completed_historical_invocations[call_id] + current_response_invocations: dict[str, tuple[str, str, str]] = {} ( processed_response.mcp_approval_requests, @@ -440,7 +480,7 @@ def should_keep( "Use a unique call ID for each tool invocation." ) - historical_identity = completed_historical_invocations.get(identity[1]) + historical_identity = get_historical_identity(identity[1]) if historical_identity is not None: if historical_identity != identity: raise ModelBehaviorError( @@ -984,9 +1024,54 @@ async def _execute_tool_plan( ) if parallel: sibling_category_failure = asyncio.Event() + native_category_failure = asyncio.Event() + native_tasks: set[asyncio.Task[Any]] = set() + cancelled_native_tasks: set[asyncio.Task[Any]] = set() + function_error_selected = False + + def on_function_error_selected() -> None: + nonlocal function_error_selected + function_error_selected = True + native_category_failure.set() + for task in native_tasks: + if not task.done(): + cancelled_native_tasks.add(task) + task.cancel() + if on_tool_error_selected is not None: + on_tool_error_selected() + + async def run_native_category( + execute: Callable[[], Awaitable[list[RunItem]]], + ) -> list[RunItem]: + # A function failure may be selected before this category is scheduled. + if function_error_selected: + return [] + task = asyncio.current_task() + assert task is not None + native_tasks.add(task) + try: + return await execute() + except asyncio.CancelledError: + if not function_error_selected: + raise + # The function category owns this failure. Let its original error + # reach gather after bounded cleanup, rather than replacing it with + # the cancellation used to stop native invocations. Cancellation of + # the parent still propagates through gather_with_cancel itself. + return [] + except Exception as error: + if task not in cancelled_native_tasks: + raise + log_tool_action_error( + "Native tool failed during sibling cancellation cleanup", error + ) + return [] + finally: + native_tasks.discard(task) def on_category_failure(error: BaseException) -> None: sibling_category_failure.set() + native_category_failure.set() if on_tool_error_selected is not None: on_tool_error_selected() if isinstance(error, asyncio.CancelledError): @@ -1007,58 +1092,76 @@ def on_category_failure(error: BaseException) -> None: context_wrapper=context_wrapper, config=run_config, isolate_parallel_failures=isolate_function_tool_failures, - on_tool_error_selected=on_tool_error_selected, + on_tool_error_selected=on_function_error_selected, sibling_category_failure=sibling_category_failure, tool_output_committer=tool_output_committer, tool_input_guardrail_results=tool_input_guardrail_results, tool_output_guardrail_results=tool_output_guardrail_results, ), - execute_computer_actions( - public_agent=public_agent, - actions=plan.computer_actions, - hooks=hooks, - context_wrapper=context_wrapper, - config=run_config, - tool_output_committer=tool_output_committer, - sibling_category_failure=sibling_category_failure, + run_native_category( + partial( + execute_computer_actions, + public_agent=public_agent, + actions=plan.computer_actions, + hooks=hooks, + context_wrapper=context_wrapper, + config=run_config, + tool_output_committer=tool_output_committer, + sibling_category_failure=native_category_failure, + ) ), - execute_custom_tool_calls( - public_agent=public_agent, - calls=plan.custom_tool_calls, - hooks=hooks, - context_wrapper=context_wrapper, - config=run_config, - tool_output_committer=tool_output_committer, - sibling_category_failure=sibling_category_failure, + run_native_category( + partial( + execute_custom_tool_calls, + public_agent=public_agent, + calls=plan.custom_tool_calls, + hooks=hooks, + context_wrapper=context_wrapper, + config=run_config, + tool_output_committer=tool_output_committer, + sibling_category_failure=native_category_failure, + ) ), - execute_shell_calls( - public_agent=public_agent, - calls=plan.shell_calls, - hooks=hooks, - context_wrapper=context_wrapper, - config=run_config, - tool_output_committer=tool_output_committer, - sibling_category_failure=sibling_category_failure, + run_native_category( + partial( + execute_shell_calls, + public_agent=public_agent, + calls=plan.shell_calls, + hooks=hooks, + context_wrapper=context_wrapper, + config=run_config, + tool_output_committer=tool_output_committer, + sibling_category_failure=native_category_failure, + ) ), - execute_apply_patch_calls( - public_agent=public_agent, - calls=plan.apply_patch_calls, - hooks=hooks, - context_wrapper=context_wrapper, - config=run_config, - tool_output_committer=tool_output_committer, - sibling_category_failure=sibling_category_failure, + run_native_category( + partial( + execute_apply_patch_calls, + public_agent=public_agent, + calls=plan.apply_patch_calls, + hooks=hooks, + context_wrapper=context_wrapper, + config=run_config, + tool_output_committer=tool_output_committer, + sibling_category_failure=native_category_failure, + ) ), - execute_local_shell_calls( - public_agent=public_agent, - calls=plan.local_shell_calls, - hooks=hooks, - context_wrapper=context_wrapper, - config=run_config, - tool_output_committer=tool_output_committer, - sibling_category_failure=sibling_category_failure, + run_native_category( + partial( + execute_local_shell_calls, + public_agent=public_agent, + calls=plan.local_shell_calls, + hooks=hooks, + context_wrapper=context_wrapper, + config=run_config, + tool_output_committer=tool_output_committer, + sibling_category_failure=native_category_failure, + ) ), on_child_failure=on_category_failure, + cancelled_children=cancelled_native_tasks, + # Preserve both the finalization wait and its cancellation-cleanup window. + cancelled_children_timeout=2 * tool_execution._FUNCTION_TOOL_POST_INVOKE_WAIT_SECONDS, ) else: ( diff --git a/src/agents/run_internal/turn_preparation.py b/src/agents/run_internal/turn_preparation.py index da531afb6a..1347dab50e 100644 --- a/src/agents/run_internal/turn_preparation.py +++ b/src/agents/run_internal/turn_preparation.py @@ -143,25 +143,31 @@ def get_model(agent: Agent[Any], run_config: RunConfig) -> Model: return run_config.model_provider.get_model(agent.model) -def _implicit_model_settings_for_agent(agent: Agent[Any]) -> ModelSettings: - if agent.model is None: - return get_default_model_settings() - if isinstance(agent.model, str): - return get_default_model_settings(agent.model) - return ModelSettings() - - -def _model_settings_for_resolved_name(agent: Agent[Any], run_config: RunConfig) -> ModelSettings: - if isinstance(run_config.model, str): - return get_default_model_settings(run_config.model) - if isinstance(run_config.model, Model): +def _model_settings_for_resolved_name( + agent: Agent[Any], run_config: RunConfig, model: Model +) -> ModelSettings: + selection = run_config.model if run_config.model is not None else agent.model + if isinstance(selection, Model): return ModelSettings() - return _implicit_model_settings_for_agent(agent) - - -def get_model_settings(agent: Agent[Any], run_config: RunConfig) -> ModelSettings: + if isinstance(selection, str) and "/" not in selection: + return get_default_model_settings(selection) + defaults = model.get_default_model_settings() + if defaults is not None: + return defaults + if isinstance(selection, str): + return get_default_model_settings(selection) + return agent.model_settings + + +def get_model_settings( + agent: Agent[Any], + run_config: RunConfig, + model: Model, + *, + prepared_overrides: ModelSettings | None = None, +) -> ModelSettings: """Resolve model settings, keeping implicit defaults aligned with the resolved model name.""" model_settings = agent.model_settings - if model_settings == _implicit_model_settings_for_agent(agent): - model_settings = _model_settings_for_resolved_name(agent, run_config) - return model_settings.resolve(run_config.model_settings) + if agent._uses_implicit_model_settings(): + model_settings = _model_settings_for_resolved_name(agent, run_config, model) + return model_settings.resolve(prepared_overrides).resolve(run_config.model_settings) diff --git a/src/agents/run_internal/turn_resolution.py b/src/agents/run_internal/turn_resolution.py index 9c4debf1e1..e16aa40ca9 100644 --- a/src/agents/run_internal/turn_resolution.py +++ b/src/agents/run_internal/turn_resolution.py @@ -602,9 +602,34 @@ def nest_history( actual_handoff.tool_call, invocation_role="handoff", ) - new_agent: Agent[Any] = await handoff.on_invoke_handoff( - context_wrapper, actual_handoff.tool_call.arguments - ) + try: + new_agent: Agent[Any] = await handoff.on_invoke_handoff( + context_wrapper, actual_handoff.tool_call.arguments + ) + except Exception as error: + if handoff.failure_error_function is None: + raise + # A failed invocation is not a transfer. Do not run transfer hooks or filters. + span_handoff.set_error(SpanError(message="Handoff invocation failed", data={})) + error_output = handoff.failure_error_function(context_wrapper, error) + if inspect.isawaitable(error_output): + error_output = await error_output + output_item = ToolCallOutputItem( + agent=public_agent, + output=error_output, + raw_item=ItemHelpers.tool_call_output_item(actual_handoff.tool_call, error_output), + ) + new_step_items.append(output_item) + _register_tool_call_items(context_wrapper, [output_item]) + return SingleStepResult( + original_input=original_input, + model_response=new_response, + pre_step_items=pre_step_items, + new_step_items=new_step_items, + next_step=NextStepRunAgain(), + tool_input_guardrail_results=list(tool_input_guardrail_results or []), + tool_output_guardrail_results=list(tool_output_guardrail_results or []), + ) register_agent_tool_configuration(new_agent) span_handoff.span_data.to_agent = new_agent.name if multiple_handoffs: diff --git a/src/agents/run_state.py b/src/agents/run_state.py index 32b6473412..ccad79c63c 100644 --- a/src/agents/run_state.py +++ b/src/agents/run_state.py @@ -1716,10 +1716,20 @@ def _serialize_context_payload( ) if hasattr(raw_context_payload, "model_dump"): - try: - serialized = raw_context_payload.model_dump(exclude_unset=True) - except TypeError: - serialized = raw_context_payload.model_dump() + if isinstance(raw_context_payload, BaseModel): + try: + # Checkpoints need current defaults and in-place changes as well as assignments. + serialized = raw_context_payload.model_dump(mode="json") + except ValueError as exc: + raise UserError( + "RunState could not serialize the Pydantic context to JSON. " + "Provide context_serializer to serialize or exclude unsupported fields." + ) from exc + else: + try: + serialized = raw_context_payload.model_dump(exclude_unset=True) + except TypeError: + serialized = raw_context_payload.model_dump() if not isinstance(serialized, Mapping): raise UserError("RunState context model_dump must return a mapping.") # We can persist the data, but the original type is lost unless the caller rebuilds it. diff --git a/src/agents/sandbox/_mount_security.py b/src/agents/sandbox/_mount_security.py index 3fe32fccc8..bf799d228b 100644 --- a/src/agents/sandbox/_mount_security.py +++ b/src/agents/sandbox/_mount_security.py @@ -242,12 +242,6 @@ "agents.extensions.sandbox.modal.mounts", "ModalCloudBucketMountStrategy", ), - "runloop_cloud_bucket": ( - "in_container", - "runloop", - "agents.extensions.sandbox.runloop.mounts", - "RunloopCloudBucketMountStrategy", - ), "vercel_cloud_bucket": ( "in_container", "vercel", @@ -264,7 +258,6 @@ "daytona_cloud_bucket": frozenset({"type", "pattern"}), "e2b_cloud_bucket": frozenset({"type", "pattern"}), "modal_cloud_bucket": frozenset({"type", "secret_name", "secret_environment_name"}), - "runloop_cloud_bucket": frozenset({"type", "pattern"}), "vercel_cloud_bucket": frozenset({"type"}), } _SERIALIZED_PATTERN_CLASS_BY_TYPE: dict[str, type[MountPatternBase]] = { @@ -295,7 +288,6 @@ class _InContainerMountCredentialCapability: "in_container", "daytona_cloud_bucket", "e2b_cloud_bucket", - "runloop_cloud_bucket", } ) _IN_CONTAINER_MOUNT_CREDENTIAL_CAPABILITIES: tuple[_InContainerMountCredentialCapability, ...] = ( diff --git a/src/agents/sandbox/runtime.py b/src/agents/sandbox/runtime.py index b527986871..5e10527d13 100644 --- a/src/agents/sandbox/runtime.py +++ b/src/agents/sandbox/runtime.py @@ -11,6 +11,7 @@ from ..exceptions import UserError from ..items import TResponseInputItem from ..logger import log_model_and_tool_action_warning +from ..model_settings import ModelSettings from ..result import RunResult, RunResultStreaming from ..run_config import RunConfig from ..run_context import RunContextWrapper, TContext @@ -275,6 +276,9 @@ async def prepare_agent( bindings=bind_execution_agent( public_agent=current_agent, execution_agent=prepared_agent, + model_settings_overrides=ModelSettings( + extra_args=prepared_agent.model_settings.extra_args + ), ), input=prepared_input, ) @@ -302,6 +306,9 @@ async def prepare_agent( bindings=bind_execution_agent( public_agent=current_agent, execution_agent=prepared_agent, + model_settings_overrides=ModelSettings( + extra_args=prepared_agent.model_settings.extra_args + ), ), input=prepared_input, ) diff --git a/src/agents/sandbox/session/base_sandbox_session.py b/src/agents/sandbox/session/base_sandbox_session.py index 3f28fc1104..73d19184be 100644 --- a/src/agents/sandbox/session/base_sandbox_session.py +++ b/src/agents/sandbox/session/base_sandbox_session.py @@ -1019,13 +1019,13 @@ async def _check_read_with_exec( ) -> Path: workspace_path = await self._validate_path_access(path) path_arg = sandbox_path_str(workspace_path) - cmd = ("sh", "-lc", '[ -r "$1" ]', "sh", path_arg) + cmd = ("sh", "-c", '[ -r "$1" ]', "sh", path_arg) result = await self.exec(*cmd, shell=False, user=user) if not result.ok(): await self._raise_read_error_from_exec( path=posix_path_as_path(coerce_posix_path(path)), workspace_path=workspace_path, - command=("sh", "-lc", "", path_arg), + command=("sh", "-c", "", path_arg), result=result, user=user, ) @@ -1075,13 +1075,13 @@ async def _check_write_with_exec( ) -> Path: workspace_path = await self._validate_path_access(path, for_write=True) path_arg = sandbox_path_str(workspace_path) - cmd = ("sh", "-lc", _WRITE_ACCESS_CHECK_SCRIPT, "sh", path_arg) + cmd = ("sh", "-c", _WRITE_ACCESS_CHECK_SCRIPT, "sh", path_arg) result = await self.exec(*cmd, shell=False, user=user) if not result.ok(): raise WorkspaceArchiveWriteError( path=workspace_path, context={ - "command": ["sh", "-lc", "", path_arg], + "command": ["sh", "-c", "", path_arg], "stdout": result.stdout.decode("utf-8", errors="replace"), "stderr": result.stderr.decode("utf-8", errors="replace"), }, @@ -1098,7 +1098,7 @@ async def _check_mkdir_with_exec( workspace_path = await self._validate_path_access(path, for_write=True) parents_flag = "1" if parents else "0" path_arg = sandbox_path_str(workspace_path) - cmd = ("sh", "-lc", _MKDIR_ACCESS_CHECK_SCRIPT, "sh", path_arg, parents_flag) + cmd = ("sh", "-c", _MKDIR_ACCESS_CHECK_SCRIPT, "sh", path_arg, parents_flag) result = await self.exec(*cmd, shell=False, user=user) if not result.ok(): raise WorkspaceArchiveWriteError( @@ -1106,7 +1106,7 @@ async def _check_mkdir_with_exec( context={ "command": [ "sh", - "-lc", + "-c", "", path_arg, parents_flag, @@ -1127,7 +1127,7 @@ async def _check_rm_with_exec( workspace_path = await self._validate_path_access(path, for_write=True) recursive_flag = "1" if recursive else "0" path_arg = sandbox_path_str(workspace_path) - cmd = ("sh", "-lc", _RM_ACCESS_CHECK_SCRIPT, "sh", path_arg, recursive_flag) + cmd = ("sh", "-c", _RM_ACCESS_CHECK_SCRIPT, "sh", path_arg, recursive_flag) result = await self.exec(*cmd, shell=False, user=user) if not result.ok(): raise WorkspaceArchiveWriteError( @@ -1135,7 +1135,7 @@ async def _check_rm_with_exec( context={ "command": [ "sh", - "-lc", + "-c", "", path_arg, recursive_flag, @@ -1297,7 +1297,7 @@ async def same_file( test = '[ "$1" -ef "$2" ]' if not follow_symlinks: test = '[ ! -L "$1" ] && [ ! -L "$2" ] && ' + test - cmd = ("sh", "-lc", test, "sh", left_arg, right_arg) + cmd = ("sh", "-c", test, "sh", left_arg, right_arg) result = await self.exec(*cmd, shell=False, user=user) if result.exit_code == 0: return True @@ -1308,7 +1308,7 @@ async def same_file( if result.exit_code == 1: return False raise ExecNonZeroError( - result, command=("sh", "-lc", "", left_arg, right_arg) + result, command=("sh", "-c", "", left_arg, right_arg) ) async def mkdir( diff --git a/src/agents/sandbox/session/runtime_helpers.py b/src/agents/sandbox/session/runtime_helpers.py index cc1236ce56..ccf429267e 100644 --- a/src/agents/sandbox/session/runtime_helpers.py +++ b/src/agents/sandbox/session/runtime_helpers.py @@ -270,7 +270,7 @@ tar_cmd="$tar_cmd -C $(quote_sh "$workspace_root") -cf - ." workspace_fingerprint=$( - sh -lc "$tar_cmd" | hash_stdin + sh -c "$tar_cmd" | hash_stdin ) fingerprint=$( printf '%s\\n%s\\n' "$workspace_fingerprint" "$manifest_digest" | hash_stdin diff --git a/src/agents/util/_asyncio_tasks.py b/src/agents/util/_asyncio_tasks.py index f557f672c8..b0ffb5b3ac 100644 --- a/src/agents/util/_asyncio_tasks.py +++ b/src/agents/util/_asyncio_tasks.py @@ -2,7 +2,7 @@ import asyncio from collections.abc import Awaitable, Callable -from typing import Any, TypeVar, overload +from typing import Any, TypeVar, cast, overload T = TypeVar("T") T1 = TypeVar("T1") @@ -30,6 +30,8 @@ async def gather_with_cancel( /, *, on_child_failure: Callable[[BaseException], None] | None = None, + cancelled_children: set[asyncio.Task[Any]] | None = None, + cancelled_children_timeout: float = 0.0, ) -> tuple[T1, T2]: ... @@ -41,6 +43,8 @@ async def gather_with_cancel( /, *, on_child_failure: Callable[[BaseException], None] | None = None, + cancelled_children: set[asyncio.Task[Any]] | None = None, + cancelled_children_timeout: float = 0.0, ) -> tuple[T1, T2, T3]: ... @@ -53,6 +57,8 @@ async def gather_with_cancel( /, *, on_child_failure: Callable[[BaseException], None] | None = None, + cancelled_children: set[asyncio.Task[Any]] | None = None, + cancelled_children_timeout: float = 0.0, ) -> tuple[T1, T2, T3, T4]: ... @@ -66,6 +72,8 @@ async def gather_with_cancel( /, *, on_child_failure: Callable[[BaseException], None] | None = None, + cancelled_children: set[asyncio.Task[Any]] | None = None, + cancelled_children_timeout: float = 0.0, ) -> tuple[T1, T2, T3, T4, T5]: ... @@ -80,6 +88,8 @@ async def gather_with_cancel( /, *, on_child_failure: Callable[[BaseException], None] | None = None, + cancelled_children: set[asyncio.Task[Any]] | None = None, + cancelled_children_timeout: float = 0.0, ) -> tuple[T1, T2, T3, T4, T5, T6]: ... @@ -87,22 +97,34 @@ async def gather_with_cancel( async def gather_with_cancel( *awaitables: Awaitable[T], on_child_failure: Callable[[BaseException], None] | None = None, + cancelled_children: set[asyncio.Task[Any]] | None = None, + cancelled_children_timeout: float = 0.0, ) -> tuple[T, ...]: ... async def gather_with_cancel( *awaitables: Awaitable[Any], on_child_failure: Callable[[BaseException], None] | None = None, + cancelled_children: set[asyncio.Task[Any]] | None = None, + cancelled_children_timeout: float = 0.0, ) -> tuple[Any, ...]: - """Gather awaitables, cancelling and draining siblings when one raises.""" + """Gather awaitables, cancelling and draining siblings when one raises. + + ``cancelled_children`` records children whose cancellation cleanup is already + owned by the caller. A sibling failure gives them ``cancelled_children_timeout`` + seconds to finish before cancelling again. Parent cancellation always cancels + all unfinished children without waiting for that grace period. + """ tasks = [asyncio.ensure_future(awaitable) for awaitable in awaitables] gather_future = asyncio.gather(*tasks) gather_future.add_done_callback(_consume_future_exception) + child_failed = False try: await asyncio.wait((gather_future,)) try: return tuple(gather_future.result()) except BaseException as error: + child_failed = True if on_child_failure is not None: on_child_failure(error) raise @@ -110,8 +132,25 @@ async def gather_with_cancel( # Coroutine closure cannot suspend; the owner must tear down child tasks. raise except BaseException: + owned_pending: list[asyncio.Future[Any]] = [] for task in tasks: - if not task.done(): + if task.done(): + continue + if child_failed and cancelled_children is not None and task in cancelled_children: + owned_pending.append(task) + else: + task.cancel() + if owned_pending: + try: + _, pending = await asyncio.wait(owned_pending, timeout=cancelled_children_timeout) + except BaseException: + # Unlike gather, wait does not forward parent cancellation to children. + for task in tasks: + if not task.done(): + task.cancel() + await asyncio.gather(*tasks, return_exceptions=True) + raise + for task in cast(set[asyncio.Task[Any]], pending): task.cancel() await asyncio.gather(*tasks, return_exceptions=True) raise diff --git a/tests/extensions/sandbox/test_daytona.py b/tests/extensions/sandbox/test_daytona.py index 0b20700095..2438bff26a 100644 --- a/tests/extensions/sandbox/test_daytona.py +++ b/tests/extensions/sandbox/test_daytona.py @@ -1088,7 +1088,7 @@ async def test_mkdir_as_user_checks_permissions_then_uses_files_api( "sandbox-user", "--", "sh", - "-lc", + "-c", _MKDIR_ACCESS_CHECK_SCRIPT, "sh", f"{daytona_module.DEFAULT_DAYTONA_WORKSPACE_ROOT}/nested", diff --git a/tests/extensions/sandbox/test_runloop.py b/tests/extensions/sandbox/test_runloop.py deleted file mode 100644 index 5f6efd85e8..0000000000 --- a/tests/extensions/sandbox/test_runloop.py +++ /dev/null @@ -1,3147 +0,0 @@ -from __future__ import annotations - -import asyncio -import builtins -import importlib -import io -import json -import shlex -import sys -import tarfile -import types -from pathlib import Path, PurePosixPath -from typing import Any, Literal, cast - -import pytest -from pydantic import BaseModel, Field, PrivateAttr - -from agents import Agent -from agents.run_context import RunContextWrapper -from agents.run_state import RunState -from agents.sandbox import Manifest, SandboxPathGrant -from agents.sandbox.capabilities import Shell -from agents.sandbox.capabilities.tools.shell_tool import ExecCommandArgs, ExecCommandTool -from agents.sandbox.entries import ( - File, - InContainerMountStrategy, - Mount, - MountpointMountPattern, - RcloneMountPattern, - S3Mount, -) -from agents.sandbox.entries.mounts.base import InContainerMountAdapter -from agents.sandbox.errors import MountConfigError -from agents.sandbox.manifest import Environment -from agents.sandbox.materialization import MaterializedFile -from agents.sandbox.session.base_sandbox_session import BaseSandboxSession -from agents.sandbox.session.dependencies import Dependencies -from agents.sandbox.session.sandbox_client import BaseSandboxClientOptions -from agents.sandbox.session.sandbox_session_state import SandboxSessionState -from agents.sandbox.snapshot import NoopSnapshot, SnapshotBase -from agents.sandbox.types import ExposedPortEndpoint -from tests.utils.factories import make_run_state - - -class _RestorableSnapshot(SnapshotBase): - type: Literal["test-restorable-runloop"] = "test-restorable-runloop" - payload: bytes = b"restored" - - async def persist( - self, - data: io.IOBase, - *, - dependencies: Dependencies | None = None, - ) -> None: - _ = (data, dependencies) - - async def restore(self, *, dependencies: Dependencies | None = None) -> io.IOBase: - _ = dependencies - return io.BytesIO(self.payload) - - async def restorable(self, *, dependencies: Dependencies | None = None) -> bool: - _ = dependencies - return True - - -class _DependencyAwareSnapshot(SnapshotBase): - type: Literal["test-restorable-runloop-deps"] = "test-restorable-runloop-deps" - payload: bytes = b"restored" - _restorable_dependencies: list[Dependencies | None] = PrivateAttr(default_factory=list) - _restore_dependencies: list[Dependencies | None] = PrivateAttr(default_factory=list) - - @property - def restorable_dependencies(self) -> list[Dependencies | None]: - return self._restorable_dependencies - - @property - def restore_dependencies(self) -> list[Dependencies | None]: - return self._restore_dependencies - - async def persist( - self, - data: io.IOBase, - *, - dependencies: Dependencies | None = None, - ) -> None: - _ = (data, dependencies) - - async def restore(self, *, dependencies: Dependencies | None = None) -> io.IOBase: - self._restore_dependencies.append(dependencies) - return io.BytesIO(self.payload) - - async def restorable(self, *, dependencies: Dependencies | None = None) -> bool: - self._restorable_dependencies.append(dependencies) - return True - - -class _FakeRunloopError(Exception): - pass - - -class _FakeAPIError(_FakeRunloopError): - def __init__( - self, - message: str, - *, - url: str = "https://api.runloop.ai/v1/test", - method: str = "POST", - body: object | None = None, - ) -> None: - super().__init__(message) - self.message = message - self.request = types.SimpleNamespace(url=url, method=method) - self.body = body - - -class _FakeAPIConnectionError(_FakeAPIError): - def __init__( - self, - message: str = "Connection error.", - *, - url: str = "https://api.runloop.ai/v1/test", - method: str = "POST", - ) -> None: - super().__init__(message, url=url, method=method, body=None) - - -class _FakeAPITimeoutError(_FakeAPIConnectionError): - def __init__( - self, - *, - url: str = "https://api.runloop.ai/v1/test", - method: str = "POST", - ) -> None: - super().__init__("Request timed out.", url=url, method=method) - - -class _FakeAPIStatusError(_FakeAPIError): - def __init__( - self, - status_code: int, - *, - body: object | None = None, - url: str = "https://api.runloop.ai/v1/test", - method: str = "POST", - message: str | None = None, - ) -> None: - super().__init__(message or f"HTTP {status_code}", url=url, method=method, body=body) - self.status_code = status_code - self.response = types.SimpleNamespace( - status_code=status_code, - request=types.SimpleNamespace(url=url, method=method), - ) - - -class _FakeAPIResponseValidationError(_FakeAPIError): - def __init__( - self, - *, - status_code: int = 500, - body: object | None = None, - url: str = "https://api.runloop.ai/v1/test", - method: str = "POST", - message: str = "Data returned by API invalid for expected schema.", - ) -> None: - super().__init__(message, url=url, method=method, body=body) - self.status_code = status_code - self.response = types.SimpleNamespace( - status_code=status_code, - request=types.SimpleNamespace(url=url, method=method), - ) - - -class _FakeAuthenticationError(_FakeAPIStatusError): - def __init__( - self, - message: str = "authentication failed", - *, - body: object | None = None, - url: str = "https://api.runloop.ai/v1/test", - method: str = "POST", - ) -> None: - super().__init__(401, body=body, url=url, method=method, message=message) - - -class _FakeBadRequestError(_FakeAPIStatusError): - def __init__( - self, - message: str = "bad request", - *, - body: object | None = None, - url: str = "https://api.runloop.ai/v1/test", - method: str = "POST", - ) -> None: - super().__init__(400, body=body, url=url, method=method, message=message) - - -class _FakeInternalServerError(_FakeAPIStatusError): - def __init__( - self, - message: str = "internal server error", - *, - body: object | None = None, - url: str = "https://api.runloop.ai/v1/test", - method: str = "POST", - ) -> None: - super().__init__(500, body=body, url=url, method=method, message=message) - - -class _FakeNotFoundError(_FakeAPIStatusError): - def __init__( - self, - message: str = "not found", - *, - body: object | None = None, - url: str = "https://api.runloop.ai/v1/test", - method: str = "GET", - ) -> None: - super().__init__(404, body=body, url=url, method=method, message=message) - - -class _FakePermissionDeniedError(_FakeAPIStatusError): - def __init__( - self, - message: str = "permission denied", - *, - body: object | None = None, - url: str = "https://api.runloop.ai/v1/test", - method: str = "POST", - ) -> None: - super().__init__(403, body=body, url=url, method=method, message=message) - - -class _FakeRateLimitError(_FakeAPIStatusError): - def __init__( - self, - message: str = "rate limited", - *, - body: object | None = None, - url: str = "https://api.runloop.ai/v1/test", - method: str = "POST", - ) -> None: - super().__init__(429, body=body, url=url, method=method, message=message) - - -class _FakeUnprocessableEntityError(_FakeAPIStatusError): - def __init__( - self, - message: str = "unprocessable entity", - *, - body: object | None = None, - url: str = "https://api.runloop.ai/v1/test", - method: str = "POST", - ) -> None: - super().__init__(422, body=body, url=url, method=method, message=message) - - -class _FakeExecutionResult: - def __init__(self, *, stdout: str = "", stderr: str = "", exit_code: int | None = 0) -> None: - self._stdout = stdout - self._stderr = stderr - self.exit_code = exit_code - - async def stdout(self, num_lines: int | None = None) -> str: - _ = num_lines - return self._stdout - - async def stderr(self, num_lines: int | None = None) -> str: - _ = num_lines - return self._stderr - - -class _FakeExecution: - _counter = 0 - - def __init__( - self, - *, - devbox: _FakeDevbox, - devbox_id: str, - command: str, - stdout_cb: object | None, - stderr_cb: object | None, - shell_name: str | None, - attach_stdin: bool, - home_dir: str, - ) -> None: - type(self)._counter += 1 - self._devbox = devbox - self.execution_id = f"exec-{type(self)._counter}" - self.devbox_id = devbox_id - self.command = command - self.shell_name = shell_name - self.attach_stdin = attach_stdin - self._stdout_cb = stdout_cb - self._stderr_cb = stderr_cb - self._done = asyncio.Event() - self._stdout = "" - self._stderr = "" - self._exit_code: int | None = None - self._killed = False - self._home_dir = home_dir - self._interactive = attach_stdin and ( - "python3 -i" in command or "python3" == command.strip() - ) - self._sleep_forever = "sleep-forever" in command - if self._interactive: - self._emit(stdout_cb, ">>> ") - elif "emit-after-result" in command: - asyncio.get_running_loop().call_soon(self._emit, stdout_cb, "final chunk\n") - self._exit_code = 0 - self._done.set() - elif "echo hello" in command: - self._stdout = "hello\n" - self._emit(stdout_cb, self._stdout) - self._exit_code = 0 - self._done.set() - elif " tar -C " in command or command.startswith("tar -C "): - self._apply_tar_extract() - self._exit_code = 0 - self._done.set() - elif self._is_resolve_workspace_path_command(command): - self._resolve_workspace_path(command) - self._done.set() - elif " cat -- " in command or command.startswith("cat -- "): - self._stdout = self._read_file_text(command) - self._emit(stdout_cb, self._stdout) - self._exit_code = 0 - self._done.set() - elif " rm -f -- " in command or command.startswith("rm -f -- "): - self._remove_file(command) - self._exit_code = 0 - self._done.set() - elif "pwd" in command: - self._stdout = f"{self._home_dir}\n" - self._emit(stdout_cb, self._stdout) - self._exit_code = 0 - self._done.set() - elif self._sleep_forever: - return - else: - self._exit_code = 0 - self._done.set() - - def _emit(self, callback: object | None, text: str) -> None: - if callback is None: - return - cast(Any, callback)(text) - - def _command_tokens(self) -> list[str]: - return shlex.split(self.command) - - def _path_relative_to_home(self, raw_path: str) -> str: - normalized = PurePosixPath(raw_path) - home = PurePosixPath(self._home_dir) - try: - relative = normalized.relative_to(home) - except ValueError: - return normalized.as_posix().lstrip("/") - rel_str = relative.as_posix() - return rel_str if rel_str else "." - - def _is_resolve_workspace_path_command(self, command: str) -> bool: - tokens = shlex.split(command) - return any( - token.startswith("/tmp/openai-agents/bin/resolve-workspace-path-") - and len(tokens) >= index + 4 - for index, token in enumerate(tokens) - ) - - def _resolve_fake_path(self, raw_path: str, *, depth: int = 0) -> PurePosixPath: - if depth > 64: - raise RuntimeError(f"symlink resolution depth exceeded: {raw_path}") - - path = PurePosixPath(raw_path) - if not path.is_absolute(): - path = PurePosixPath(self._home_dir) / path - - parts = path.parts - current = PurePosixPath("/") - for index, part in enumerate(parts[1:], start=1): - current = current / part - target = self._devbox.symlinks.get(current.as_posix()) - if target is None: - continue - - target_path = PurePosixPath(target) - if not target_path.is_absolute(): - target_path = current.parent / target_path - for remaining in parts[index + 1 :]: - target_path /= remaining - return self._resolve_fake_path(target_path.as_posix(), depth=depth + 1) - - return path - - @staticmethod - def _fake_path_is_under(path: PurePosixPath, root: PurePosixPath) -> bool: - return path == root or root in path.parents - - def _resolve_workspace_path(self, command: str) -> None: - tokens = self._command_tokens() - helper_index = next( - index - for index, token in enumerate(tokens) - if token.startswith("/tmp/openai-agents/bin/resolve-workspace-path-") - ) - root = self._resolve_fake_path(tokens[helper_index + 1]) - candidate = self._resolve_fake_path(tokens[helper_index + 2]) - for_write = tokens[helper_index + 3] - grant_tokens = tokens[helper_index + 4 :] - - if self._fake_path_is_under(candidate, root): - self._stdout = f"{candidate.as_posix()}\n" - self._exit_code = 0 - return - - best_grant: tuple[PurePosixPath, str, str] | None = None - for index in range(0, len(grant_tokens), 2): - grant_original = grant_tokens[index] - read_only = grant_tokens[index + 1] - grant_root = self._resolve_fake_path(grant_original) - if not self._fake_path_is_under(candidate, grant_root): - continue - if best_grant is None or len(grant_root.parts) > len(best_grant[0].parts): - best_grant = (grant_root, grant_original, read_only) - - if best_grant is not None: - _grant_root, grant_original, read_only = best_grant - if for_write == "1" and read_only == "1": - self._stderr = ( - f"read-only extra path grant: {grant_original}\n" - f"resolved path: {candidate.as_posix()}\n" - ) - self._exit_code = 114 - return - self._stdout = f"{candidate.as_posix()}\n" - self._exit_code = 0 - return - - self._stderr = f"workspace escape: {candidate.as_posix()}\n" - self._exit_code = 111 - - def _apply_tar_extract(self) -> None: - tokens = self._command_tokens() - tar_index = tokens.index("tar") - root = tokens[tar_index + 2] - archive_path = tokens[tar_index + 4] - archive_rel = self._path_relative_to_home(archive_path) - root_rel = self._path_relative_to_home(root) - payload = self._devbox.files[archive_rel] - with tarfile.open(fileobj=io.BytesIO(payload), mode="r:*") as archive: - for member in archive.getmembers(): - if member.isdir(): - continue - fileobj = archive.extractfile(member) - if fileobj is None: - continue - target = PurePosixPath(member.name) - if root_rel != ".": - target = PurePosixPath(root_rel) / target - self._devbox.files[target.as_posix()] = fileobj.read() - - def _read_file_text(self, command: str) -> str: - tokens = shlex.split(command) - path = tokens[-1] - rel_path = self._path_relative_to_home(path) - return self._devbox.files.get(rel_path, b"").decode("utf-8", errors="replace") - - def _remove_file(self, command: str) -> None: - tokens = shlex.split(command) - path = tokens[-1] - rel_path = self._path_relative_to_home(path) - self._devbox.files.pop(rel_path, None) - - async def result(self, timeout: float | None = None) -> _FakeExecutionResult: - _ = timeout - await self._done.wait() - return _FakeExecutionResult( - stdout=self._stdout, - stderr=self._stderr, - exit_code=self._exit_code, - ) - - async def kill(self, timeout: float | None = None) -> None: - _ = timeout - self._killed = True - self._exit_code = -9 - self._done.set() - - async def send_input(self, text: str) -> None: - if not self._interactive: - return - if text == "5 + 5\n": - self._stdout += "10\n>>> " - self._emit(self._stdout_cb, "10\n>>> ") - return - if text in {"exit()\n", "exit\n"}: - self._exit_code = 0 - self._done.set() - return - - -class _FakeExecutionsAPI: - send_std_in_calls: list[tuple[str, str, str]] - - def __init__(self, owner: _FakeAsyncRunloopSDK) -> None: - self._owner = owner - self.send_std_in_calls = [] - - async def send_std_in( - self, - execution_id: str, - *, - devbox_id: str, - text: str | None = None, - timeout: float | None = None, - **_: object, - ) -> object: - del timeout - self.send_std_in_calls.append((execution_id, devbox_id, text or "")) - execution = self._owner.executions[execution_id] - await execution.send_input(text or "") - return types.SimpleNamespace(success=True) - - -class _FakeFileInterface: - def __init__(self, devbox: _FakeDevbox) -> None: - self._devbox = devbox - - def _file_key(self, path: str) -> str: - normalized = PurePosixPath(path) - home = PurePosixPath(self._devbox.home_dir) - try: - relative = normalized.relative_to(home) - except ValueError: - return normalized.as_posix() - rel_str = relative.as_posix() - return rel_str if rel_str else "." - - async def download(self, *, path: str, timeout: float | None = None, **_: object) -> bytes: - del timeout - self._devbox.file_download_paths.append(path) - key = self._file_key(path) - if key not in self._devbox.files: - raise _FakeNotFoundError(path) - return self._devbox.files[key] - - async def upload( - self, - *, - path: str, - file: bytes, - timeout: float | None = None, - **_: object, - ) -> object: - del timeout - self._devbox.file_upload_paths.append(path) - self._devbox.files[self._file_key(path)] = bytes(file) - return {} - - -class _FakeNetworkInterface: - def __init__(self, devbox: _FakeDevbox) -> None: - self._devbox = devbox - - async def enable_tunnel(self, **params: object) -> object: - self._devbox.enable_tunnel_calls.append(dict(params)) - self._devbox.tunnel_key = "test-key" - return types.SimpleNamespace(tunnel_key="test-key") - - -class _FakeCommandInterface: - def __init__(self, devbox: _FakeDevbox) -> None: - self._devbox = devbox - - async def exec(self, command: str, **params: object) -> _FakeExecutionResult: - execution = _FakeExecution( - devbox=self._devbox, - devbox_id=self._devbox.id, - command=command, - stdout_cb=params.get("stdout"), - stderr_cb=params.get("stderr"), - shell_name=cast(str | None, params.get("shell_name")), - attach_stdin=bool(params.get("attach_stdin", False)), - home_dir=self._devbox.home_dir, - ) - self._devbox.owner.executions[execution.execution_id] = execution - self._devbox.exec_calls.append((command, dict(params))) - return await execution.result() - - async def exec_async(self, command: str, **params: object) -> _FakeExecution: - execution = _FakeExecution( - devbox=self._devbox, - devbox_id=self._devbox.id, - command=command, - stdout_cb=params.get("stdout"), - stderr_cb=params.get("stderr"), - shell_name=cast(str | None, params.get("shell_name")), - attach_stdin=bool(params.get("attach_stdin", False)), - home_dir=self._devbox.home_dir, - ) - self._devbox.owner.executions[execution.execution_id] = execution - self._devbox.exec_async_calls.append((command, dict(params))) - return execution - - -class _FakeDevbox: - def __init__( - self, - owner: _FakeAsyncRunloopSDK, - *, - devbox_id: str, - status: str = "running", - snapshot_source_id: str | None = None, - environment_variables: dict[str, str] | None = None, - launch_parameters: dict[str, object] | None = None, - ) -> None: - self.owner = owner - self.id = devbox_id - self.status = status - self.snapshot_source_id = snapshot_source_id - self.environment_variables = dict(environment_variables or {}) - self.launch_parameters = dict(launch_parameters or {}) - user_parameters = self.launch_parameters.get("user_parameters") - if isinstance(user_parameters, dict): - username = user_parameters.get("username") - uid = user_parameters.get("uid") - if username == "root" and uid == 0: - self.home_dir = "/root" - elif isinstance(username, str) and username: - self.home_dir = f"/home/{username}" - else: - self.home_dir = "/home/user" - else: - self.home_dir = "/home/user" - self.files: dict[str, bytes] = {} - self.symlinks: dict[str, str] = {} - self.file_download_paths: list[str] = [] - self.file_upload_paths: list[str] = [] - self.tunnel_key: str | None = None - self.enable_tunnel_calls: list[dict[str, object]] = [] - self.exec_calls: list[tuple[str, dict[str, object]]] = [] - self.exec_async_calls: list[tuple[str, dict[str, object]]] = [] - self.snapshot_calls: list[dict[str, object]] = [] - self.shutdown_calls = 0 - self.suspend_calls = 0 - self.resume_calls = 0 - self.await_running_calls = 0 - self.resume_returns_before_running = False - self.cmd = _FakeCommandInterface(self) - self.file = _FakeFileInterface(self) - self.net = _FakeNetworkInterface(self) - - async def get_info(self, timeout: float | None = None, **_: object) -> object: - del timeout - tunnel = ( - types.SimpleNamespace(tunnel_key=self.tunnel_key) - if self.tunnel_key is not None - else None - ) - return types.SimpleNamespace(status=self.status, tunnel=tunnel) - - async def get_tunnel_url( - self, - port: int, - timeout: float | None = None, - **_: object, - ) -> str | None: - del timeout - if self.tunnel_key is None: - return None - return f"https://{port}-{self.tunnel_key}.tunnel.runloop.ai" - - async def snapshot_disk(self, **params: object) -> object: - self.snapshot_calls.append(dict(params)) - snapshot_id = f"snap-{len(self.snapshot_calls)}" - return types.SimpleNamespace(id=snapshot_id) - - async def shutdown(self, timeout: float | None = None, **_: object) -> object: - del timeout - self.shutdown_calls += 1 - self.status = "shutdown" - return types.SimpleNamespace(status=self.status) - - async def suspend(self, timeout: float | None = None, **_: object) -> object: - del timeout - self.suspend_calls += 1 - self.status = "suspended" - return types.SimpleNamespace(status=self.status) - - async def await_suspended(self) -> object: - return types.SimpleNamespace(status="suspended") - - async def await_running(self, **_: object) -> object: - self.await_running_calls += 1 - self.status = "running" - return types.SimpleNamespace(status=self.status) - - async def resume(self, timeout: float | None = None, **_: object) -> object: - del timeout - self.resume_calls += 1 - if self.resume_returns_before_running: - self.status = "resuming" - return types.SimpleNamespace(status=self.status) - self.status = "running" - return types.SimpleNamespace(status=self.status) - - -class _FakeDevboxOps: - def __init__(self, owner: _FakeAsyncRunloopSDK) -> None: - self._owner = owner - self.create_calls: list[dict[str, object]] = [] - self.create_from_snapshot_calls: list[tuple[str, dict[str, object]]] = [] - self.from_id_calls: list[str] = [] - self.devboxes: dict[str, _FakeDevbox] = {} - self._counter = 0 - - def _new_devbox( - self, - *, - snapshot_source_id: str | None = None, - environment_variables: dict[str, str] | None = None, - launch_parameters: dict[str, object] | None = None, - ) -> _FakeDevbox: - self._counter += 1 - devbox = _FakeDevbox( - self._owner, - devbox_id=f"devbox-{self._counter}", - snapshot_source_id=snapshot_source_id, - environment_variables=environment_variables, - launch_parameters=launch_parameters, - ) - self.devboxes[devbox.id] = devbox - return devbox - - async def create(self, **params: object) -> _FakeDevbox: - self.create_calls.append(dict(params)) - return self._new_devbox( - environment_variables=cast(dict[str, str] | None, params.get("environment_variables")), - launch_parameters=cast(dict[str, object] | None, params.get("launch_parameters")), - ) - - async def create_from_snapshot(self, snapshot_id: str, **params: object) -> _FakeDevbox: - self.create_from_snapshot_calls.append((snapshot_id, dict(params))) - return self._new_devbox( - snapshot_source_id=snapshot_id, - environment_variables=cast(dict[str, str] | None, params.get("environment_variables")), - launch_parameters=cast(dict[str, object] | None, params.get("launch_parameters")), - ) - - def from_id(self, devbox_id: str) -> _FakeDevbox: - self.from_id_calls.append(devbox_id) - if devbox_id not in self.devboxes: - raise _FakeNotFoundError(devbox_id) - return self.devboxes[devbox_id] - - -class _FakeBlueprint: - def __init__( - self, owner: _FakeAsyncRunloopSDK, *, blueprint_id: str, name: str | None = None - ) -> None: - self.owner = owner - self.id = blueprint_id - self.name = name or blueprint_id - self.logs_calls: list[dict[str, object]] = [] - self.delete_calls: list[dict[str, object]] = [] - - async def get_info(self, **_: object) -> object: - return types.SimpleNamespace(id=self.id, name=self.name, status="build_complete") - - async def logs(self, **params: object) -> object: - self.logs_calls.append(dict(params)) - return types.SimpleNamespace(items=[f"log:{self.id}"]) - - async def delete(self, **params: object) -> object: - self.delete_calls.append(dict(params)) - return types.SimpleNamespace(id=self.id, deleted=True) - - -class _FakeBlueprintOps: - def __init__(self, owner: _FakeAsyncRunloopSDK) -> None: - self._owner = owner - self.create_calls: list[dict[str, object]] = [] - self.list_calls: list[dict[str, object]] = [] - self.from_id_calls: list[str] = [] - self.blueprints: dict[str, _FakeBlueprint] = {} - self._counter = 0 - - def _new_blueprint(self, *, name: str | None = None) -> _FakeBlueprint: - self._counter += 1 - blueprint = _FakeBlueprint( - self._owner, - blueprint_id=f"blueprint-{self._counter}", - name=name, - ) - self.blueprints[blueprint.id] = blueprint - return blueprint - - async def create(self, **params: object) -> _FakeBlueprint: - self.create_calls.append(dict(params)) - return self._new_blueprint(name=cast(str | None, params.get("name"))) - - async def list(self, **params: object) -> list[_FakeBlueprint]: - self.list_calls.append(dict(params)) - return list(self.blueprints.values()) - - def from_id(self, blueprint_id: str) -> _FakeBlueprint: - self.from_id_calls.append(blueprint_id) - return self.blueprints.setdefault( - blueprint_id, - _FakeBlueprint(self._owner, blueprint_id=blueprint_id, name=blueprint_id), - ) - - -class _FakeBlueprintsAPI: - def __init__(self, owner: _FakeAsyncRunloopSDK) -> None: - self._owner = owner - self.list_public_calls: list[dict[str, object]] = [] - self.logs_calls: list[tuple[str, dict[str, object]]] = [] - self.await_build_complete_calls: list[tuple[str, dict[str, object]]] = [] - - async def list_public(self, **params: object) -> object: - self.list_public_calls.append(dict(params)) - return types.SimpleNamespace(data=list(self._owner.blueprint.blueprints.values())) - - async def logs(self, blueprint_id: str, **params: object) -> object: - self.logs_calls.append((blueprint_id, dict(params))) - return types.SimpleNamespace(items=[f"log:{blueprint_id}"]) - - async def await_build_complete(self, blueprint_id: str, **params: object) -> object: - self.await_build_complete_calls.append((blueprint_id, dict(params))) - blueprint = self._owner.blueprint.from_id(blueprint_id) - return types.SimpleNamespace(id=blueprint.id, status="build_complete") - - -class _FakeBenchmarkRun: - def __init__(self, *, run_id: str, benchmark_id: str) -> None: - self.id = run_id - self.benchmark_id = benchmark_id - - async def get_info(self, **_: object) -> object: - return types.SimpleNamespace(id=self.id, benchmark_id=self.benchmark_id) - - -class _FakeBenchmark: - def __init__( - self, owner: _FakeAsyncRunloopSDK, *, benchmark_id: str, name: str | None = None - ) -> None: - self.owner = owner - self.id = benchmark_id - self.name = name or benchmark_id - self.update_calls: list[dict[str, object]] = [] - self.start_run_calls: list[dict[str, object]] = [] - - async def get_info(self, **_: object) -> object: - return types.SimpleNamespace(id=self.id, name=self.name) - - async def update(self, **params: object) -> object: - self.update_calls.append(dict(params)) - return types.SimpleNamespace(id=self.id, name=params.get("name", self.name)) - - async def start_run(self, **params: object) -> _FakeBenchmarkRun: - self.start_run_calls.append(dict(params)) - return _FakeBenchmarkRun(run_id=f"run-{self.id}", benchmark_id=self.id) - - async def list_runs(self, **_: object) -> list[_FakeBenchmarkRun]: - return [_FakeBenchmarkRun(run_id=f"run-{self.id}", benchmark_id=self.id)] - - -class _FakeBenchmarkOps: - def __init__(self, owner: _FakeAsyncRunloopSDK) -> None: - self._owner = owner - self.create_calls: list[dict[str, object]] = [] - self.list_calls: list[dict[str, object]] = [] - self.from_id_calls: list[str] = [] - self.benchmarks: dict[str, _FakeBenchmark] = {} - self._counter = 0 - - def _new_benchmark(self, *, name: str | None = None) -> _FakeBenchmark: - self._counter += 1 - benchmark = _FakeBenchmark( - self._owner, benchmark_id=f"benchmark-{self._counter}", name=name - ) - self.benchmarks[benchmark.id] = benchmark - return benchmark - - async def create(self, **params: object) -> _FakeBenchmark: - self.create_calls.append(dict(params)) - return self._new_benchmark(name=cast(str | None, params.get("name"))) - - async def list(self, **params: object) -> list[_FakeBenchmark]: - self.list_calls.append(dict(params)) - return list(self.benchmarks.values()) - - def from_id(self, benchmark_id: str) -> _FakeBenchmark: - self.from_id_calls.append(benchmark_id) - return self.benchmarks.setdefault( - benchmark_id, - _FakeBenchmark(self._owner, benchmark_id=benchmark_id, name=benchmark_id), - ) - - -class _FakeBenchmarksAPI: - def __init__(self, owner: _FakeAsyncRunloopSDK) -> None: - self._owner = owner - self.list_public_calls: list[dict[str, object]] = [] - self.definitions_calls: list[tuple[str, dict[str, object]]] = [] - self.update_scenarios_calls: list[tuple[str, dict[str, object]]] = [] - - async def list_public(self, **params: object) -> object: - self.list_public_calls.append(dict(params)) - return types.SimpleNamespace(data=list(self._owner.benchmark.benchmarks.values())) - - async def definitions(self, benchmark_id: str, **params: object) -> object: - self.definitions_calls.append((benchmark_id, dict(params))) - return types.SimpleNamespace(definitions=[types.SimpleNamespace(id=f"def-{benchmark_id}")]) - - async def update_scenarios(self, benchmark_id: str, **params: object) -> object: - self.update_scenarios_calls.append((benchmark_id, dict(params))) - return types.SimpleNamespace(id=benchmark_id, **dict(params)) - - -class _FakeSecret: - def __init__( - self, owner: _FakeAsyncRunloopSDK, *, name: str, value: str, secret_id: str - ) -> None: - self.owner = owner - self.name = name - self.value = value - self.id = secret_id - self.update_calls: list[tuple[str, dict[str, object]]] = [] - self.delete_calls: list[dict[str, object]] = [] - - async def get_info(self, **_: object) -> object: - return types.SimpleNamespace(id=self.id, name=self.name) - - async def update(self, value: str, **params: object) -> _FakeSecret: - self.update_calls.append((value, dict(params))) - self.value = value - return self - - async def delete(self, **params: object) -> object: - self.delete_calls.append(dict(params)) - self.owner.secret.secrets.pop(self.name, None) - return types.SimpleNamespace(id=self.id, name=self.name, deleted=True) - - -class _FakeSecretOps: - def __init__(self, owner: _FakeAsyncRunloopSDK) -> None: - self._owner = owner - self.create_calls: list[tuple[str, str, dict[str, object]]] = [] - self.update_calls: list[tuple[str, str, dict[str, object]]] = [] - self.delete_calls: list[tuple[str, dict[str, object]]] = [] - self.list_calls: list[dict[str, object]] = [] - self.secrets: dict[str, _FakeSecret] = {} - self._counter = 0 - self.conflict_status_code = 409 - self.conflict_body: object | None = {"error": "secret exists"} - self.conflict_message: str | None = None - - def _new_secret(self, *, name: str, value: str) -> _FakeSecret: - self._counter += 1 - secret = _FakeSecret( - self._owner, name=name, value=value, secret_id=f"secret-{self._counter}" - ) - self.secrets[name] = secret - return secret - - async def create(self, name: str, value: str, **params: object) -> _FakeSecret: - self.create_calls.append((name, value, dict(params))) - if name in self.secrets: - raise _FakeAPIStatusError( - self.conflict_status_code, - body=self.conflict_body, - message=self.conflict_message, - ) - return self._new_secret(name=name, value=value) - - async def list(self, **params: object) -> list[_FakeSecret]: - self.list_calls.append(dict(params)) - return list(self.secrets.values()) - - async def update(self, secret: _FakeSecret | str, value: str, **params: object) -> _FakeSecret: - name = secret.name if isinstance(secret, _FakeSecret) else secret - self.update_calls.append((name, value, dict(params))) - secret_obj = self.secrets[name] - secret_obj.value = value - return secret_obj - - async def delete(self, secret: _FakeSecret | str, **params: object) -> object: - name = secret.name if isinstance(secret, _FakeSecret) else secret - self.delete_calls.append((name, dict(params))) - secret_obj = self.secrets.pop(name) - return types.SimpleNamespace(id=secret_obj.id, name=name, deleted=True) - - -class _FakeSecretsAPI: - def __init__(self, owner: _FakeAsyncRunloopSDK) -> None: - self._owner = owner - self.retrieve_calls: list[tuple[str, dict[str, object]]] = [] - - async def retrieve(self, name: str, **params: object) -> object: - self.retrieve_calls.append((name, dict(params))) - secret = self._owner.secret.secrets[name] - return types.SimpleNamespace(id=secret.id, name=secret.name) - - -class _FakeNetworkPolicy: - def __init__( - self, owner: _FakeAsyncRunloopSDK, *, policy_id: str, name: str | None = None - ) -> None: - self.owner = owner - self.id = policy_id - self.name = name or policy_id - self.update_calls: list[dict[str, object]] = [] - self.delete_calls: list[dict[str, object]] = [] - - async def get_info(self, **_: object) -> object: - return types.SimpleNamespace(id=self.id, name=self.name) - - async def update(self, **params: object) -> object: - self.update_calls.append(dict(params)) - return types.SimpleNamespace(id=self.id, name=params.get("name", self.name)) - - async def delete(self, **params: object) -> object: - self.delete_calls.append(dict(params)) - self.owner.network_policy.policies.pop(self.id, None) - return types.SimpleNamespace(id=self.id, deleted=True) - - -class _FakeNetworkPolicyOps: - def __init__(self, owner: _FakeAsyncRunloopSDK) -> None: - self._owner = owner - self.create_calls: list[dict[str, object]] = [] - self.list_calls: list[dict[str, object]] = [] - self.from_id_calls: list[str] = [] - self.policies: dict[str, _FakeNetworkPolicy] = {} - self._counter = 0 - - def _new_policy(self, *, name: str | None = None) -> _FakeNetworkPolicy: - self._counter += 1 - policy = _FakeNetworkPolicy(self._owner, policy_id=f"policy-{self._counter}", name=name) - self.policies[policy.id] = policy - return policy - - async def create(self, **params: object) -> _FakeNetworkPolicy: - self.create_calls.append(dict(params)) - return self._new_policy(name=cast(str | None, params.get("name"))) - - async def list(self, **params: object) -> list[_FakeNetworkPolicy]: - self.list_calls.append(dict(params)) - return list(self.policies.values()) - - def from_id(self, network_policy_id: str) -> _FakeNetworkPolicy: - self.from_id_calls.append(network_policy_id) - return self.policies.setdefault( - network_policy_id, - _FakeNetworkPolicy(self._owner, policy_id=network_policy_id, name=network_policy_id), - ) - - -class _FakeNetworkPoliciesAPI: - def __init__(self, owner: _FakeAsyncRunloopSDK) -> None: - self._owner = owner - self.retrieve_calls: list[tuple[str, dict[str, object]]] = [] - - async def retrieve(self, network_policy_id: str, **params: object) -> object: - self.retrieve_calls.append((network_policy_id, dict(params))) - policy = self._owner.network_policy.from_id(network_policy_id) - return types.SimpleNamespace(id=policy.id, name=policy.name) - - -class _FakeAxonSql: - def __init__(self) -> None: - self.query_calls: list[dict[str, object]] = [] - self.batch_calls: list[dict[str, object]] = [] - - async def query(self, **params: object) -> object: - self.query_calls.append(dict(params)) - return types.SimpleNamespace(rows=[["ok"]]) - - async def batch(self, **params: object) -> object: - self.batch_calls.append(dict(params)) - return types.SimpleNamespace(results=[types.SimpleNamespace(success=True)]) - - -class _FakeAxon: - def __init__( - self, owner: _FakeAsyncRunloopSDK, *, axon_id: str, name: str | None = None - ) -> None: - self.owner = owner - self.id = axon_id - self.name = name or axon_id - self.publish_calls: list[dict[str, object]] = [] - self.sql = _FakeAxonSql() - - async def get_info(self, **_: object) -> object: - return types.SimpleNamespace(id=self.id, name=self.name) - - async def publish(self, **params: object) -> object: - self.publish_calls.append(dict(params)) - return types.SimpleNamespace(published=True) - - -class _FakeAxonOps: - def __init__(self, owner: _FakeAsyncRunloopSDK) -> None: - self._owner = owner - self.create_calls: list[dict[str, object]] = [] - self.list_calls: list[dict[str, object]] = [] - self.from_id_calls: list[str] = [] - self.axons: dict[str, _FakeAxon] = {} - self._counter = 0 - - def _new_axon(self, *, name: str | None = None) -> _FakeAxon: - self._counter += 1 - axon = _FakeAxon(self._owner, axon_id=f"axon-{self._counter}", name=name) - self.axons[axon.id] = axon - return axon - - async def create(self, **params: object) -> _FakeAxon: - self.create_calls.append(dict(params)) - return self._new_axon(name=cast(str | None, params.get("name"))) - - async def list(self, **params: object) -> list[_FakeAxon]: - self.list_calls.append(dict(params)) - return list(self.axons.values()) - - def from_id(self, axon_id: str) -> _FakeAxon: - self.from_id_calls.append(axon_id) - return self.axons.setdefault( - axon_id, - _FakeAxon(self._owner, axon_id=axon_id, name=axon_id), - ) - - -class _FakeLaunchAfterIdle(BaseModel): - idle_time_seconds: int - on_idle: Literal["shutdown", "suspend"] - - def to_dict( - self, - *, - mode: str = "python", - exclude_none: bool = False, - exclude_defaults: bool = False, - ) -> dict[str, object]: - return cast( - dict[str, object], - self.model_dump( - mode=cast(Literal["json", "python"], mode), - exclude_none=exclude_none, - exclude_defaults=exclude_defaults, - ), - ) - - -class _FakeUserParameters(BaseModel): - username: str - uid: int - - def to_dict( - self, - *, - mode: str = "python", - exclude_none: bool = False, - exclude_defaults: bool = False, - ) -> dict[str, object]: - return cast( - dict[str, object], - self.model_dump( - mode=cast(Literal["json", "python"], mode), - exclude_none=exclude_none, - exclude_defaults=exclude_defaults, - ), - ) - - -class _FakeLaunchParameters(BaseModel): - network_policy_id: str | None = None - resource_size_request: ( - Literal["X_SMALL", "SMALL", "MEDIUM", "LARGE", "X_LARGE", "XX_LARGE", "CUSTOM_SIZE"] | None - ) = None - custom_cpu_cores: float | None = None - custom_gb_memory: int | None = None - custom_disk_size: int | None = None - architecture: Literal["x86_64", "arm64"] | None = None - keep_alive_time_seconds: int | None = None - after_idle: _FakeLaunchAfterIdle | dict[str, object] | None = None - launch_commands: list[str] | tuple[str, ...] | None = None - required_services: list[str] | tuple[str, ...] | None = None - user_parameters: dict[str, object] | None = None - - def to_dict( - self, - *, - mode: str = "python", - exclude_none: bool = False, - exclude_defaults: bool = False, - ) -> dict[str, object]: - return cast( - dict[str, object], - self.model_dump( - mode=cast(Literal["json", "python"], mode), - exclude_none=exclude_none, - exclude_defaults=exclude_defaults, - ), - ) - - -class _FakeAsyncRunloopSDK: - created_instances: list[_FakeAsyncRunloopSDK] = [] - - def __init__( - self, - *, - bearer_token: str | None = None, - base_url: str | None = None, - **_: object, - ) -> None: - self.bearer_token = bearer_token - self.base_url = base_url or "https://api.runloop.ai" - self.executions: dict[str, _FakeExecution] = {} - self.devbox = _FakeDevboxOps(self) - self.blueprint = _FakeBlueprintOps(self) - self.benchmark = _FakeBenchmarkOps(self) - self.secret = _FakeSecretOps(self) - self.network_policy = _FakeNetworkPolicyOps(self) - self.axon = _FakeAxonOps(self) - self.api = types.SimpleNamespace( - devboxes=types.SimpleNamespace(executions=_FakeExecutionsAPI(self)), - blueprints=_FakeBlueprintsAPI(self), - benchmarks=_FakeBenchmarksAPI(self), - secrets=_FakeSecretsAPI(self), - network_policies=_FakeNetworkPoliciesAPI(self), - ) - type(self).created_instances.append(self) - - async def aclose(self) -> None: - return None - - -def _load_runloop_module(monkeypatch: pytest.MonkeyPatch) -> Any: - _FakeAsyncRunloopSDK.created_instances.clear() - _FakeExecution._counter = 0 - fake_runloop: Any = types.ModuleType("runloop_api_client") - fake_runloop.APIConnectionError = _FakeAPIConnectionError - fake_runloop.APIResponseValidationError = _FakeAPIResponseValidationError - fake_runloop.APITimeoutError = _FakeAPITimeoutError - fake_runloop.APIStatusError = _FakeAPIStatusError - fake_runloop.AuthenticationError = _FakeAuthenticationError - fake_runloop.BadRequestError = _FakeBadRequestError - fake_runloop.InternalServerError = _FakeInternalServerError - fake_runloop.NotFoundError = _FakeNotFoundError - fake_runloop.PermissionDeniedError = _FakePermissionDeniedError - fake_runloop.RateLimitError = _FakeRateLimitError - fake_runloop.RunloopError = _FakeRunloopError - fake_runloop.UnprocessableEntityError = _FakeUnprocessableEntityError - - fake_sdk: Any = types.ModuleType("runloop_api_client.sdk") - fake_sdk.AsyncRunloopSDK = _FakeAsyncRunloopSDK - - fake_types: Any = types.ModuleType("runloop_api_client.types") - fake_types.AfterIdle = _FakeLaunchAfterIdle - fake_types.LaunchParameters = _FakeLaunchParameters - fake_shared: Any = types.ModuleType("runloop_api_client.types.shared") - fake_launch_parameters_module: Any = types.ModuleType( - "runloop_api_client.types.shared.launch_parameters" - ) - fake_launch_parameters_module.UserParameters = _FakeUserParameters - fake_shared.launch_parameters = fake_launch_parameters_module - fake_types.shared = fake_shared - - monkeypatch.setitem(sys.modules, "runloop_api_client", fake_runloop) - monkeypatch.setitem(sys.modules, "runloop_api_client.sdk", fake_sdk) - monkeypatch.setitem(sys.modules, "runloop_api_client.types", fake_types) - monkeypatch.setitem(sys.modules, "runloop_api_client.types.shared", fake_shared) - monkeypatch.setitem( - sys.modules, - "runloop_api_client.types.shared.launch_parameters", - fake_launch_parameters_module, - ) - # Re-importing an adapter also replaces its registered model classes. - # Record undo before clearing these entries for fresh class registration; - # unrelated registrations must survive teardown. - monkeypatch.setitem( - SandboxSessionState._subclass_registry, - "runloop", - SandboxSessionState._subclass_registry.get("runloop", SandboxSessionState), - ) - del SandboxSessionState._subclass_registry["runloop"] - module_names = ( - "agents.extensions.sandbox.runloop.sandbox", - "agents.extensions.sandbox.runloop", - ) - for name in module_names: - monkeypatch.delitem(sys.modules, name, raising=False) - - module: Any = importlib.import_module("agents.extensions.sandbox.runloop.sandbox") - # Track the fresh imports too, so teardown removes them before restoring - # any original modules alongside the real provider SDK. - for name in module_names: - monkeypatch.setitem(sys.modules, name, sys.modules.pop(name)) - return module - - -def _build_tar_bytes(files: dict[str, bytes]) -> bytes: - buffer = io.BytesIO() - with tarfile.open(fileobj=buffer, mode="w") as archive: - for name, payload in files.items(): - info = tarfile.TarInfo(name=name) - info.size = len(payload) - archive.addfile(info, io.BytesIO(payload)) - return buffer.getvalue() - - -def test_runloop_package_re_exports_backend_symbols(monkeypatch: pytest.MonkeyPatch) -> None: - runloop_module = _load_runloop_module(monkeypatch) - package_module = importlib.import_module("agents.extensions.sandbox.runloop") - - assert package_module.RunloopSandboxClient is runloop_module.RunloopSandboxClient - assert package_module.RunloopPlatformClient is runloop_module.RunloopPlatformClient - assert package_module.RunloopLaunchParameters is runloop_module.RunloopLaunchParameters - assert package_module.RunloopAfterIdle is runloop_module.RunloopAfterIdle - assert package_module.RunloopUserParameters is runloop_module.RunloopUserParameters - - -@pytest.fixture(autouse=True) -def _trust_recording_mounts_for_tests(monkeypatch: pytest.MonkeyPatch) -> None: - from agents.sandbox import _mount_security - - original = _mount_security._mount_class_is_trusted - monkeypatch.setattr( - _mount_security, - "_mount_class_is_trusted", - lambda mount: isinstance(mount, _RecordingMount) or original(mount), - ) - - -class _RecordingMount(Mount): - type: str = "runloop_recording_mount" - mount_strategy: InContainerMountStrategy = Field( - default_factory=lambda: InContainerMountStrategy(pattern=MountpointMountPattern()) - ) - _mounted_paths: list[Path] = PrivateAttr(default_factory=list) - _unmounted_paths: list[Path] = PrivateAttr(default_factory=list) - - def supported_in_container_patterns( - self, - ) -> tuple[builtins.type[MountpointMountPattern], ...]: - return (MountpointMountPattern,) - - def in_container_adapter(self) -> InContainerMountAdapter: - mount = self - - class _Adapter(InContainerMountAdapter): - def validate(self, strategy: InContainerMountStrategy) -> None: - _ = strategy - - async def activate( - self, - strategy: InContainerMountStrategy, - session: BaseSandboxSession, - dest: Path, - base_dir: Path, - ) -> list[MaterializedFile]: - _ = (strategy, session, base_dir) - path = mount._resolve_mount_path(session, dest) - mount._mounted_paths.append(path) - return [] - - async def deactivate( - self, - strategy: InContainerMountStrategy, - session: BaseSandboxSession, - dest: Path, - base_dir: Path, - ) -> None: - _ = (strategy, session, base_dir) - path = mount._resolve_mount_path(session, dest) - mount._unmounted_paths.append(path) - - async def teardown_for_snapshot( - self, - strategy: InContainerMountStrategy, - session: BaseSandboxSession, - path: Path, - ) -> None: - _ = (strategy, session) - mount._unmounted_paths.append(path) - - async def restore_after_snapshot( - self, - strategy: InContainerMountStrategy, - session: BaseSandboxSession, - path: Path, - ) -> None: - _ = (strategy, session) - mount._mounted_paths.append(path) - - return _Adapter(self) - - -class TestRunloopSandbox: - @pytest.mark.asyncio - async def test_runloop_does_not_advertise_pty_support( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - - assert session.supports_pty() is False - - @pytest.mark.asyncio - async def test_create_uses_runloop_default_workspace_root( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - - assert session.state.manifest.root == runloop_module.DEFAULT_RUNLOOP_WORKSPACE_ROOT - - @pytest.mark.asyncio - async def test_create_uses_root_workspace_root_when_root_launch_enabled( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions( - user_parameters=runloop_module.RunloopUserParameters( - username="root", - uid=0, - ), - ) - ) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - - assert session.state.manifest.root == runloop_module.DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT - assert sdk.devbox.create_calls[0]["launch_parameters"] == { - "user_parameters": {"username": "root", "uid": 0} - } - - def test_runloop_sdk_backed_user_parameters_construct_from_extension_exports( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - user_parameters = runloop_module.RunloopUserParameters(username="user", uid=1000) - - assert user_parameters.username == "user" - assert user_parameters.uid == 1000 - assert user_parameters.to_dict(mode="json", exclude_none=True) == { - "username": "user", - "uid": 1000, - } - - @pytest.mark.asyncio - async def test_create_normalizes_dict_user_parameters( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions( - user_parameters={"username": "root", "uid": 0}, - ) - ) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - - assert sdk.devbox.create_calls[0]["launch_parameters"] == { - "user_parameters": {"username": "root", "uid": 0} - } - assert session.state.user_parameters is not None - assert session.state.user_parameters.username == "root" - assert session.state.user_parameters.uid == 0 - - @pytest.mark.asyncio - async def test_empty_manifest_exec_succeeds_immediately_after_start_non_root( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - manifest=Manifest(root=f"{runloop_module.DEFAULT_RUNLOOP_WORKSPACE_ROOT}/project"), - options=runloop_module.RunloopSandboxClientOptions(), - ) - await session.start() - result = await session.exec("pwd", shell=False) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - command, _ = devbox.exec_calls[-1] - - assert result.ok() - assert "cd /home/user/project &&" in command - - @pytest.mark.asyncio - async def test_empty_manifest_exec_succeeds_immediately_after_start_root( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - manifest=Manifest(root="/root/project"), - options=runloop_module.RunloopSandboxClientOptions( - user_parameters=runloop_module.RunloopUserParameters( - username="root", - uid=0, - ) - ), - ) - await session.start() - result = await session.exec("pwd", shell=False) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - command, _ = devbox.exec_calls[-1] - - assert result.ok() - assert "cd /root/project &&" in command - - @pytest.mark.asyncio - async def test_create_merges_env_vars_with_manifest_precedence( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - await client.create( - manifest=Manifest( - root=runloop_module.DEFAULT_RUNLOOP_WORKSPACE_ROOT, - environment=Environment(value={"SHARED": "manifest", "ONLY_MANIFEST": "1"}), - ), - options=runloop_module.RunloopSandboxClientOptions( - env_vars={"SHARED": "option", "ONLY_OPTION": "1"}, - ), - ) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - - assert sdk.devbox.create_calls - create_params = sdk.devbox.create_calls[0] - assert create_params["environment_variables"] == { - "SHARED": "manifest", - "ONLY_MANIFEST": "1", - "ONLY_OPTION": "1", - } - - def test_runloop_client_options_preserve_positional_exposed_ports( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - options = runloop_module.RunloopSandboxClientOptions( - None, - None, - None, - False, - None, - None, - (8765,), - ) - - assert options.exposed_ports == (8765,) - - def test_runloop_client_options_append_new_fields_after_existing_positionals( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - options = runloop_module.RunloopSandboxClientOptions( - None, - None, - None, - False, - None, - None, - (8765,), - None, - launch_parameters=runloop_module.RunloopLaunchParameters( - network_policy_id="np-123", - ), - managed_secrets={"API_KEY": "secret"}, - ) - - assert options.exposed_ports == (8765,) - assert options.launch_parameters is not None - assert options.launch_parameters.network_policy_id == "np-123" - assert options.managed_secrets == {"API_KEY": "secret"} - - def test_runloop_sdk_backed_launch_models_construct_from_extension_exports( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - after_idle = runloop_module.RunloopAfterIdle(idle_time_seconds=300, on_idle="suspend") - launch_parameters = runloop_module.RunloopLaunchParameters( - network_policy_id="np-123", - after_idle=after_idle, - launch_commands=["echo hi"], - ) - - assert after_idle.idle_time_seconds == 300 - assert launch_parameters.after_idle is not None - assert launch_parameters.after_idle.on_idle == "suspend" - assert launch_parameters.to_dict(mode="json", exclude_none=True)["launch_commands"] == [ - "echo hi" - ] - - def test_runloop_tunnel_config_remains_extension_model( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - tunnel = runloop_module.RunloopTunnelConfig(auth_mode="authenticated") - - assert isinstance(tunnel, BaseModel) - assert tunnel.model_dump(mode="json", exclude_none=True) == {"auth_mode": "authenticated"} - - @pytest.mark.asyncio - async def test_create_passes_runloop_native_launch_options_and_persists_secret_refs( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions( - name="native-runloop", - user_parameters=runloop_module.RunloopUserParameters(username="user", uid=1000), - launch_parameters=runloop_module.RunloopLaunchParameters( - network_policy_id="np-123", - resource_size_request="MEDIUM", - custom_cpu_cores=2, - custom_gb_memory=8, - custom_disk_size=16, - architecture="arm64", - keep_alive_time_seconds=600, - after_idle=runloop_module.RunloopAfterIdle( - idle_time_seconds=300, - on_idle="suspend", - ), - launch_commands=("echo hi",), - required_services=("postgres",), - ), - tunnel=runloop_module.RunloopTunnelConfig( - auth_mode="authenticated", - http_keep_alive=True, - wake_on_http=True, - ), - gateways={ - "GWS_OPENAI": runloop_module.RunloopGatewaySpec( - gateway="openai-gateway", - secret="OPENAI_GATEWAY_SECRET", - ) - }, - mcp={ - "MCP_TOKEN": runloop_module.RunloopMcpSpec( - mcp_config="github-readonly", - secret="MCP_SECRET", - ) - }, - metadata={"team": "agents"}, - managed_secrets={"API_KEY": "super-secret"}, - ), - ) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - - assert sdk.secret.create_calls == [("API_KEY", "super-secret", {"timeout": 30.0})] - assert sdk.devbox.create_calls - create_params = sdk.devbox.create_calls[0] - assert create_params["launch_parameters"] == { - "network_policy_id": "np-123", - "resource_size_request": "MEDIUM", - "custom_cpu_cores": 2.0, - "custom_gb_memory": 8, - "custom_disk_size": 16, - "architecture": "arm64", - "keep_alive_time_seconds": 600, - "after_idle": {"idle_time_seconds": 300, "on_idle": "suspend"}, - "launch_commands": ["echo hi"], - "required_services": ["postgres"], - "user_parameters": {"username": "user", "uid": 1000}, - } - assert create_params["tunnel"] == { - "auth_mode": "authenticated", - "http_keep_alive": True, - "wake_on_http": True, - } - assert create_params["gateways"] == { - "GWS_OPENAI": {"gateway": "openai-gateway", "secret": "OPENAI_GATEWAY_SECRET"} - } - assert create_params["mcp"] == { - "MCP_TOKEN": {"mcp_config": "github-readonly", "secret": "MCP_SECRET"} - } - assert create_params["metadata"] == {"team": "agents"} - assert create_params["secrets"] == {"API_KEY": "API_KEY"} - assert session.state.secret_refs == {"API_KEY": "API_KEY"} - assert session.state.metadata == {"team": "agents"} - assert "super-secret" not in json.dumps(session.state.model_dump(mode="json")) - - @pytest.mark.asyncio - async def test_create_normalizes_dict_launch_parameters_and_tunnel_options( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions( - launch_parameters={ - "network_policy_id": "np-123", - "launch_commands": ["echo hi"], - }, - tunnel={ - "auth_mode": "authenticated", - "wake_on_http": True, - }, - ) - ) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - - assert sdk.devbox.create_calls[0]["launch_parameters"] == { - "network_policy_id": "np-123", - "launch_commands": ["echo hi"], - } - assert sdk.devbox.create_calls[0]["tunnel"] == { - "auth_mode": "authenticated", - "wake_on_http": True, - } - assert session.state.launch_parameters is not None - assert session.state.launch_parameters.network_policy_id == "np-123" - assert session.state.tunnel is not None - assert session.state.tunnel.auth_mode == "authenticated" - - @pytest.mark.asyncio - async def test_create_normalizes_dict_launch_parameters_and_tunnel_from_parsed_options( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - options = cast( - Any, - BaseSandboxClientOptions.parse( - { - "type": "runloop", - "launch_parameters": { - "network_policy_id": "np-456", - "required_services": ["postgres"], - }, - "tunnel": { - "auth_mode": "open", - "http_keep_alive": True, - }, - } - ), - ) - - assert options.type == "runloop" - assert options.launch_parameters is not None - assert options.tunnel is not None - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=options) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - - assert sdk.devbox.create_calls[0]["launch_parameters"] == { - "network_policy_id": "np-456", - "required_services": ["postgres"], - } - assert sdk.devbox.create_calls[0]["tunnel"] == { - "auth_mode": "open", - "http_keep_alive": True, - } - assert session.state.launch_parameters is not None - assert session.state.launch_parameters.network_policy_id == "np-456" - assert session.state.tunnel is not None - assert session.state.tunnel.auth_mode == "open" - - @pytest.mark.asyncio - async def test_run_state_round_trip_preserves_runloop_session_state_without_secret_values( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - agent = Agent(name="TestAgent") - context: RunContextWrapper[dict[str, str]] = RunContextWrapper(context={}) - state: RunState[dict[str, str], Agent[Any]] = make_run_state( - agent, - context=context, - original_input="test", - ) - client = runloop_module.RunloopSandboxClient(bearer_token="test-token") - session_state = runloop_module.RunloopSandboxSessionState( - manifest=Manifest(), - snapshot=NoopSnapshot(id="runloop-state"), - devbox_id="devbox-123", - launch_parameters=runloop_module.RunloopLaunchParameters(network_policy_id="np-123"), - secret_refs={"API_KEY": "API_KEY"}, - ) - serialized_session_state = client.serialize_session_state(session_state) - state._sandbox = { - "backend_id": "runloop", - "current_agent_key": agent.name, - "current_agent_name": agent.name, - "session_state": serialized_session_state, - "sessions_by_agent": { - agent.name: { - "agent_name": agent.name, - "session_state": serialized_session_state, - } - }, - } - - restored = await RunState.from_json(agent, state.to_json()) - - assert restored._sandbox is not None - restored_session_payload = cast(dict[str, object], restored._sandbox["session_state"]) - assert restored_session_payload["secret_refs"] == {"API_KEY": "API_KEY"} - assert "managed_secrets" not in restored_session_payload - assert "secret-value" not in json.dumps(restored_session_payload) - - restored_session_state = client.deserialize_session_state(restored_session_payload) - assert isinstance(restored_session_state, runloop_module.RunloopSandboxSessionState) - assert restored_session_state.secret_refs == {"API_KEY": "API_KEY"} - assert restored_session_state.launch_parameters is not None - assert restored_session_state.launch_parameters.network_policy_id == "np-123" - - await client.close() - - @pytest.mark.asyncio - async def test_create_upserts_managed_secret_when_secret_exists( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - sdk.secret._new_secret(name="API_KEY", value="old-value") - - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions( - managed_secrets={"API_KEY": "new-value"}, - ) - ) - - assert sdk.secret.create_calls == [("API_KEY", "new-value", {"timeout": 30.0})] - assert sdk.secret.update_calls == [("API_KEY", "new-value", {"timeout": 30.0})] - assert session.state.secret_refs == {"API_KEY": "API_KEY"} - - @pytest.mark.asyncio - async def test_create_upserts_managed_secret_when_runloop_returns_bad_request_exists( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - sdk.secret._new_secret(name="API_KEY", value="old-value") - sdk.secret.conflict_status_code = 400 - sdk.secret.conflict_body = { - "message": "Secret with name 'API_KEY' already exists", - } - sdk.secret.conflict_message = "Secret with name 'API_KEY' already exists" - - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions( - managed_secrets={"API_KEY": "new-value"}, - ) - ) - - assert sdk.secret.create_calls == [("API_KEY", "new-value", {"timeout": 30.0})] - assert sdk.secret.update_calls == [("API_KEY", "new-value", {"timeout": 30.0})] - assert session.state.secret_refs == {"API_KEY": "API_KEY"} - - @pytest.mark.asyncio - async def test_create_references_existing_managed_secrets_without_uploading_values( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions( - managed_secrets={ - "SHARED_TOKEN": runloop_module.RunloopExistingSecret(), - "API_KEY": runloop_module.RunloopExistingSecret(), - }, - ) - ) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - - assert sdk.secret.create_calls == [] - assert sdk.secret.update_calls == [] - create_params = sdk.devbox.create_calls[0] - assert create_params["secrets"] == { - "API_KEY": "API_KEY", - "SHARED_TOKEN": "SHARED_TOKEN", - } - assert session.state.secret_refs == { - "API_KEY": "API_KEY", - "SHARED_TOKEN": "SHARED_TOKEN", - } - - @pytest.mark.asyncio - async def test_create_mixes_existing_and_uploaded_managed_secrets( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions( - managed_secrets={ - "API_KEY": "super-secret", - "SHARED_TOKEN": runloop_module.RunloopExistingSecret(), - }, - ) - ) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - - assert sdk.secret.create_calls == [("API_KEY", "super-secret", {"timeout": 30.0})] - assert sdk.secret.update_calls == [] - assert "SHARED_TOKEN" not in sdk.secret.secrets - create_params = sdk.devbox.create_calls[0] - assert create_params["secrets"] == { - "API_KEY": "API_KEY", - "SHARED_TOKEN": "SHARED_TOKEN", - } - assert session.state.secret_refs == { - "API_KEY": "API_KEY", - "SHARED_TOKEN": "SHARED_TOKEN", - } - assert "super-secret" not in json.dumps(session.state.model_dump(mode="json")) - - def test_runloop_client_options_round_trip_existing_managed_secrets( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - options = runloop_module.RunloopSandboxClientOptions( - managed_secrets={ - "API_KEY": "super-secret", - "SHARED_TOKEN": runloop_module.RunloopExistingSecret(), - }, - ) - - restored = runloop_module.RunloopSandboxClientOptions.model_validate( - json.loads(json.dumps(options.model_dump(mode="json"))) - ) - - assert restored.managed_secrets == { - "API_KEY": "super-secret", - "SHARED_TOKEN": runloop_module.RunloopExistingSecret(), - } - - @pytest.mark.asyncio - async def test_resume_and_snapshot_restore_reuse_runloop_native_options( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions( - name="native-runloop", - launch_parameters=runloop_module.RunloopLaunchParameters( - network_policy_id="np-123", - launch_commands=("echo hi",), - ), - tunnel=runloop_module.RunloopTunnelConfig(auth_mode="open"), - gateways={ - "GWS_OPENAI": runloop_module.RunloopGatewaySpec( - gateway="openai-gateway", - secret="OPENAI_GATEWAY_SECRET", - ) - }, - mcp={ - "MCP_TOKEN": runloop_module.RunloopMcpSpec( - mcp_config="github-readonly", - secret="MCP_SECRET", - ) - }, - metadata={"team": "agents"}, - managed_secrets={"API_KEY": "super-secret"}, - ), - ) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - sdk.devbox.devboxes[session.state.devbox_id].status = "shutdown" - sdk.devbox.create_calls.clear() - - resumed = await client.resume(session.state) - await resumed._inner.hydrate_workspace( # noqa: SLF001 - io.BytesIO(runloop_module._encode_runloop_snapshot_ref(snapshot_id="snap-123")) # noqa: SLF001 - ) - - assert sdk.devbox.create_calls == [ - { - "timeout": session.state.timeouts.create_s, - "name": "native-runloop", - "launch_parameters": { - "network_policy_id": "np-123", - "launch_commands": ["echo hi"], - }, - "tunnel": {"auth_mode": "open"}, - "gateways": { - "GWS_OPENAI": { - "gateway": "openai-gateway", - "secret": "OPENAI_GATEWAY_SECRET", - } - }, - "mcp": { - "MCP_TOKEN": { - "mcp_config": "github-readonly", - "secret": "MCP_SECRET", - } - }, - "metadata": {"team": "agents"}, - "secrets": {"API_KEY": "API_KEY"}, - } - ] - assert sdk.devbox.create_from_snapshot_calls == [ - ( - "snap-123", - { - "timeout": session.state.timeouts.resume_s, - "name": "native-runloop", - "launch_parameters": { - "network_policy_id": "np-123", - "launch_commands": ["echo hi"], - }, - "tunnel": {"auth_mode": "open"}, - "gateways": { - "GWS_OPENAI": { - "gateway": "openai-gateway", - "secret": "OPENAI_GATEWAY_SECRET", - } - }, - "mcp": { - "MCP_TOKEN": { - "mcp_config": "github-readonly", - "secret": "MCP_SECRET", - } - }, - "metadata": {"team": "agents"}, - "secrets": {"API_KEY": "API_KEY"}, - }, - ) - ] - - @pytest.mark.asyncio - async def test_platform_blueprints_and_benchmarks_clients( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - blueprint = await client.platform.blueprints.create(name="bp1") - listed_blueprints = await client.platform.blueprints.list(limit=5) - public_blueprints = await client.platform.blueprints.list_public(limit=10) - await client.platform.blueprints.logs(blueprint.id) - build_info = await client.platform.blueprints.await_build_complete(blueprint.id) - await client.platform.blueprints.delete(blueprint.id) - - benchmark = await client.platform.benchmarks.create( - name="bm1", - required_secret_names=["API_KEY"], - ) - listed_benchmarks = await client.platform.benchmarks.list(limit=5) - public_benchmarks = await client.platform.benchmarks.list_public(limit=10) - await client.platform.benchmarks.update(benchmark.id, description="desc") - definitions = await client.platform.benchmarks.definitions(benchmark.id) - run = await client.platform.benchmarks.start_run(benchmark.id, run_name="eval") - scenario_update = await client.platform.benchmarks.update_scenarios( - benchmark.id, - scenarios_to_add=["scenario-1"], - ) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - - assert blueprint in listed_blueprints - assert public_blueprints.data - assert build_info.status == "build_complete" - assert sdk.api.blueprints.logs_calls == [(blueprint.id, {})] - assert sdk.api.blueprints.await_build_complete_calls == [(blueprint.id, {})] - assert benchmark in listed_benchmarks - assert public_benchmarks.data - assert definitions.definitions[0].id == f"def-{benchmark.id}" - assert run.benchmark_id == benchmark.id - assert scenario_update.scenarios_to_add == ["scenario-1"] - - @pytest.mark.asyncio - async def test_platform_secrets_network_policies_and_axons_clients( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - assert not hasattr(client.platform.axons, "subscribe_sse") - secret = await client.platform.secrets.create(name="SECRET_A", value="secret-value") - listed_secrets = await client.platform.secrets.list() - secret_info = await client.platform.secrets.get("SECRET_A") - updated_secret = await client.platform.secrets.update( - name="SECRET_A", - value="secret-value-2", - ) - deleted_secret = await client.platform.secrets.delete("SECRET_A") - - policy = await client.platform.network_policies.create(name="policy-a", allow_all=True) - listed_policies = await client.platform.network_policies.list() - await client.platform.network_policies.update(policy.id, description="limited") - deleted_policy = await client.platform.network_policies.delete(policy.id) - - axon = await client.platform.axons.create(name="axon-a") - listed_axons = await client.platform.axons.list() - publish_result = await client.platform.axons.publish( - axon.id, - event_type="task_done", - origin="AGENT_EVENT", - payload="{}", - source="agent", - ) - query_result = await client.platform.axons.query_sql(axon.id, sql="select 1") - batch_result = await client.platform.axons.batch_sql( - axon.id, - statements=[{"sql": "select 1"}], - ) - - assert secret in listed_secrets - assert secret_info.name == "SECRET_A" - assert updated_secret.name == "SECRET_A" - assert deleted_secret.name == "SECRET_A" - assert policy in listed_policies - assert deleted_policy.id == policy.id - assert axon in listed_axons - assert publish_result.published is True - assert query_result.rows == [["ok"]] - assert batch_result.results[0].success is True - - @pytest.mark.asyncio - async def test_resume_reconnects_suspended_devbox_and_skips_start( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions(pause_on_exit=True), - ) - state = cast( - Any, - client.deserialize_session_state(client.serialize_session_state(session.state)), - ) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - sdk.devbox.create_calls.clear() - sdk.devbox.devboxes[state.devbox_id].status = "suspended" - - resumed = await client.resume(state) - - assert sdk.devbox.from_id_calls == [state.devbox_id] - assert sdk.devbox.create_calls == [] - assert resumed._inner._skip_start is True # noqa: SLF001 - - @pytest.mark.asyncio - async def test_skip_start_rejects_unsafe_mount_before_provider_work( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - sentinel = "runloop-start-secret" - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - session.state.manifest = Manifest( - entries={ - "data": S3Mount( - bucket="bucket", - access_key_id="access-key", - secret_access_key=sentinel, - mount_strategy=InContainerMountStrategy(pattern=RcloneMountPattern()), - ) - } - ) - session._inner._skip_start = True # noqa: SLF001 - exec_calls_before = len(devbox.exec_calls) - resume_calls_before = devbox.resume_calls - snapshot_calls_before = len(devbox.snapshot_calls) - - with pytest.raises(MountConfigError) as exc: - await session.start() - - assert len(devbox.exec_calls) == exec_calls_before - assert devbox.resume_calls == resume_calls_before - assert len(devbox.snapshot_calls) == snapshot_calls_before - assert sentinel not in str(exc.value) - - @pytest.mark.asyncio - async def test_resume_reconnects_running_devbox_without_pause( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - await session.start() - state = session.state - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[state.devbox_id] - devbox.files["existing.txt"] = b"keep" - sdk.devbox.create_calls.clear() - - resumed = await client.resume(state) - await resumed.start() - - assert sdk.devbox.from_id_calls == [state.devbox_id] - assert sdk.devbox.create_calls == [] - assert resumed.state.devbox_id == state.devbox_id - assert resumed._inner._skip_start is False # noqa: SLF001 - assert devbox.files["existing.txt"] == b"keep" - - @pytest.mark.asyncio - async def test_resume_reconnected_devbox_without_pause_does_not_reprovision_accounts( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - session.state.snapshot = _RestorableSnapshot(id="snapshot-mismatch") - state = session.state - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - sdk.devbox.create_calls.clear() - - resumed = await client.resume(state) - inner = resumed._inner - provision_called = False - - async def _cannot_skip(self: object, *, is_running: bool) -> bool: - return False - - async def _restore(self: object) -> None: - return None - - async def _provision_accounts() -> None: - nonlocal provision_called - provision_called = True - - async def _reapply(self: object) -> None: - return None - - monkeypatch.setattr( - inner, - "_can_skip_snapshot_restore_on_resume", - types.MethodType(_cannot_skip, inner), - ) - monkeypatch.setattr( - inner, - "_restore_snapshot_into_workspace_on_resume", - types.MethodType(_restore, inner), - ) - monkeypatch.setattr(inner, "provision_manifest_accounts", _provision_accounts) - monkeypatch.setattr( - inner, - "_reapply_ephemeral_manifest_on_resume", - types.MethodType(_reapply, inner), - ) - - await resumed.start() - - assert sdk.devbox.from_id_calls == [state.devbox_id] - assert sdk.devbox.create_calls == [] - assert provision_called is False - - @pytest.mark.asyncio - async def test_resume_recreates_terminal_devbox_without_pause( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - state = session.state - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - sdk.devbox.devboxes[state.devbox_id].status = "shutdown" - sdk.devbox.create_calls.clear() - original_devbox_id = state.devbox_id - - resumed = await client.resume(state) - - assert sdk.devbox.from_id_calls == [original_devbox_id] - assert len(sdk.devbox.create_calls) == 1 - assert resumed.state.devbox_id != original_devbox_id - assert resumed._inner._skip_start is False # noqa: SLF001 - - @pytest.mark.asyncio - async def test_resume_waits_for_devbox_running_before_skip_start( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions(pause_on_exit=True), - ) - session.state.snapshot = _RestorableSnapshot(id="resume-race") - state = session.state - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - sdk.devbox.create_calls.clear() - devbox = sdk.devbox.devboxes[state.devbox_id] - devbox.status = "suspended" - devbox.resume_returns_before_running = True - - resumed = await client.resume(state) - inner = resumed._inner - - async def _can_skip(self: object, *, is_running: bool) -> bool: - return is_running - - async def _reapply(self: object) -> None: - return None - - async def _restore(self: object) -> None: - raise AssertionError("resume should wait for running instead of restoring snapshot") - - monkeypatch.setattr( - inner, - "_can_skip_snapshot_restore_on_resume", - types.MethodType(_can_skip, inner), - ) - monkeypatch.setattr( - inner, - "_reapply_ephemeral_manifest_on_resume", - types.MethodType(_reapply, inner), - ) - monkeypatch.setattr( - inner, - "_restore_snapshot_into_workspace_on_resume", - types.MethodType(_restore, inner), - ) - - await resumed.start() - - assert devbox.resume_calls == 1 - assert devbox.await_running_calls == 1 - assert devbox.status == "running" - assert sdk.devbox.create_calls == [] - assert resumed._inner._skip_start is True # noqa: SLF001 - - @pytest.mark.asyncio - async def test_skip_start_resume_passes_dependencies_to_snapshot_restorable( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - dependencies = Dependencies().bind_value("test.dep", object()) - - async with runloop_module.RunloopSandboxClient(dependencies=dependencies) as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions(pause_on_exit=True), - ) - snapshot = _DependencyAwareSnapshot(id="dep-aware") - session.state.snapshot = snapshot - state = session.state - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - sdk.devbox.devboxes[state.devbox_id].status = "suspended" - - resumed = await client.resume(state) - inner = resumed._inner - - async def _can_skip(self: object, *, is_running: bool) -> bool: - return is_running - - async def _reapply(self: object) -> None: - return None - - monkeypatch.setattr( - inner, - "_can_skip_snapshot_restore_on_resume", - types.MethodType(_can_skip, inner), - ) - monkeypatch.setattr( - inner, - "_reapply_ephemeral_manifest_on_resume", - types.MethodType(_reapply, inner), - ) - - await resumed.start() - - assert snapshot.restorable_dependencies - assert snapshot.restorable_dependencies[-1] is not None - - @pytest.mark.asyncio - async def test_root_launch_exec_and_io_use_root_home( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - manifest=Manifest(root="/root/project"), - options=runloop_module.RunloopSandboxClientOptions( - user_parameters=runloop_module.RunloopUserParameters( - username="root", - uid=0, - ) - ), - ) - await session.start() - await session.exec("pwd && echo hello", shell=True) - exec_sdk = _FakeAsyncRunloopSDK.created_instances[-1] - exec_devbox = exec_sdk.devbox.devboxes[session.state.devbox_id] - command, _ = exec_devbox.exec_calls[-1] - await session.write("/root/project/output.txt", io.BytesIO(b"hello")) - payload = await session.read("/root/project/output.txt") - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - - assert payload.read() == b"hello" - assert "cd /root/project &&" in command - assert devbox.files["project/output.txt"] == b"hello" - - @pytest.mark.asyncio - async def test_delete_shuts_down_runloop_devbox( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions(), - ) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - - await client.delete(session) - - assert devbox.shutdown_calls == 1 - assert devbox.status == "shutdown" - - @pytest.mark.asyncio - async def test_resolve_exposed_port_enables_tunnel_and_formats_endpoint( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions(exposed_ports=(4500,)), - ) - await session.start() - endpoint = await session.resolve_exposed_port(4500) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - - assert endpoint == ExposedPortEndpoint( - host="4500-test-key.tunnel.runloop.ai", - port=443, - tls=True, - ) - assert devbox.enable_tunnel_calls - - @pytest.mark.asyncio - async def test_exec_timeout_raises_for_runloop_one_shot_exec( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - await session.start() - with pytest.raises(runloop_module.ExecTimeoutError): - await session.exec("sleep-forever", shell=False, timeout=0.01) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - executions = list(sdk.executions.values()) - - assert executions - assert any("sleep-forever" in execution.command for execution in executions) - - @pytest.mark.asyncio - async def test_exec_maps_runloop_http_408_to_timeout_with_provider_context( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - await session.start() - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - - async def _raise_timeout(*args: object, **kwargs: object) -> object: - _ = (args, kwargs) - raise _FakeAPIStatusError( - 408, - body={"error": "execution timed out"}, - url=f"https://api.runloop.ai/v1/devboxes/{devbox.id}/execute", - method="POST", - ) - - monkeypatch.setattr(devbox.cmd, "exec", _raise_timeout) - - with pytest.raises(runloop_module.ExecTimeoutError) as exc_info: - await session.exec("pwd", shell=False, timeout=3.0) - - assert exc_info.value.context["http_status"] == 408 - assert exc_info.value.context["cause_type"] == "_FakeAPIStatusError" - assert exc_info.value.context["request_method"] == "POST" - assert exc_info.value.context["request_url"] == ( - f"https://api.runloop.ai/v1/devboxes/{devbox.id}/execute" - ) - assert exc_info.value.context["provider_body"] == {"error": "execution timed out"} - - @pytest.mark.asyncio - async def test_exec_maps_runloop_http_error_to_transport_with_provider_context( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - await session.start() - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - - async def _raise_rate_limit(*args: object, **kwargs: object) -> object: - _ = (args, kwargs) - raise _FakeAPIStatusError( - 429, - body={"error": "rate limited"}, - url=f"https://api.runloop.ai/v1/devboxes/{devbox.id}/execute", - method="POST", - ) - - monkeypatch.setattr(devbox.cmd, "exec", _raise_rate_limit) - - with pytest.raises(runloop_module.ExecTransportError) as exc_info: - await session.exec("pwd", shell=False) - - assert exc_info.value.context["http_status"] == 429 - assert exc_info.value.context["cause_type"] == "_FakeAPIStatusError" - assert exc_info.value.context["provider_body"] == {"error": "rate limited"} - assert exc_info.value.context["detail"] == "exec_failed" - assert exc_info.value.retryable is True - - @pytest.mark.asyncio - async def test_exec_marks_typed_runloop_bad_request_non_retryable( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - await session.start() - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - - async def _raise_bad_request(*args: object, **kwargs: object) -> object: - _ = (args, kwargs) - raise _FakeBadRequestError( - body={"error": "invalid command"}, - url=f"https://api.runloop.ai/v1/devboxes/{devbox.id}/execute", - method="POST", - ) - - monkeypatch.setattr(devbox.cmd, "exec", _raise_bad_request) - - with pytest.raises(runloop_module.ExecTransportError) as exc_info: - await session.exec("pwd", shell=False) - - assert exc_info.value.context["http_status"] == 400 - assert exc_info.value.context["cause_type"] == "_FakeBadRequestError" - assert exc_info.value.context["provider_body"] == {"error": "invalid command"} - assert exc_info.value.context["detail"] == "exec_failed" - assert exc_info.value.retryable is False - - @pytest.mark.parametrize( - ("status", "expected_retryable"), - [ - (400, False), - (401, False), - (403, False), - (404, False), - (408, True), - (422, False), - (429, True), - (500, True), - (502, True), - (503, True), - (504, True), - ], - ) - def test_runloop_retryability_status_table( - self, - monkeypatch: pytest.MonkeyPatch, - status: int, - expected_retryable: bool, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - error = _FakeAPIStatusError(status, body={"error": f"HTTP {status}"}) - - assert runloop_module._runloop_provider_retryability(error) is expected_retryable - - @pytest.mark.asyncio - async def test_exec_wraps_command_with_workspace_context( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - manifest=Manifest( - root=f"{runloop_module.DEFAULT_RUNLOOP_WORKSPACE_ROOT}/project", - environment=Environment(value={"ONLY_MANIFEST": "1"}), - ), - options=runloop_module.RunloopSandboxClientOptions(env_vars={"ONLY_OPTION": "2"}), - ) - await session.start() - await session.exec("pwd && echo hello", shell=True) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - - assert devbox.exec_calls - command, params = devbox.exec_calls[-1] - assert "cd /home/user/project &&" in command - assert "env --" in command - assert "ONLY_MANIFEST=1" in command - assert "ONLY_OPTION=2" in command - assert "attach_stdin" not in params - assert "polling_config" in params - - @pytest.mark.asyncio - async def test_read_and_write_use_normalized_absolute_paths( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - await session.start() - await session.write( - "/home/user/project/output.txt", - io.BytesIO(b"hello"), - ) - payload = await session.read("/home/user/project/output.txt") - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - - assert payload.read() == b"hello" - assert devbox.files["project/output.txt"] == b"hello" - assert devbox.file_upload_paths == ["/home/user/project/output.txt"] - assert devbox.file_download_paths == ["/home/user/project/output.txt"] - - @pytest.mark.asyncio - async def test_read_and_write_extra_path_grant_use_file_api_directly( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - manifest=Manifest( - root="/home/user/project", - extra_path_grants=(SandboxPathGrant(path="/tmp"),), - ), - options=runloop_module.RunloopSandboxClientOptions(), - ) - await session.start() - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - exec_count = len(devbox.exec_calls) - - await session.write("/tmp/output.txt", io.BytesIO(b"hello")) - payload = await session.read("/tmp/output.txt") - - assert payload.read() == b"hello" - assert devbox.files["/tmp/output.txt"] == b"hello" - assert devbox.file_upload_paths == ["/tmp/output.txt"] - assert devbox.file_download_paths == ["/tmp/output.txt"] - assert len(devbox.exec_calls) == exec_count + 7 - assert devbox.exec_calls[exec_count + 4][0] == "mkdir -p -- /tmp" - - @pytest.mark.asyncio - async def test_write_rejects_workspace_symlink_to_read_only_extra_path_grant( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - manifest=Manifest( - root="/home/user/project", - extra_path_grants=(SandboxPathGrant(path="/tmp/protected", read_only=True),), - ), - options=runloop_module.RunloopSandboxClientOptions(), - ) - await session.start() - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - devbox.symlinks["/home/user/project/link"] = "/tmp/protected" - - with pytest.raises(runloop_module.WorkspaceArchiveWriteError) as exc_info: - await session.write("link/result.txt", io.BytesIO(b"blocked")) - - assert devbox.file_upload_paths == [] - assert str(exc_info.value) == ( - "failed to write archive for path: /home/user/project/link/result.txt" - ) - assert exc_info.value.context == { - "path": "/home/user/project/link/result.txt", - "reason": "read_only_extra_path_grant", - "grant_path": "/tmp/protected", - "resolved_path": "/tmp/protected/result.txt", - } - - @pytest.mark.asyncio - async def test_read_wraps_runloop_http_error_with_provider_context( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - await session.start() - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - - async def _raise_download_error(**kwargs: object) -> bytes: - _ = kwargs - raise _FakeAPIStatusError( - 500, - body={"error": "download failed"}, - url=f"https://api.runloop.ai/v1/devboxes/{devbox.id}/files/project/output.txt", - method="GET", - ) - - monkeypatch.setattr(devbox.file, "download", _raise_download_error) - - with pytest.raises(runloop_module.WorkspaceArchiveReadError) as exc_info: - await session.read("/home/user/project/output.txt") - - assert exc_info.value.context["http_status"] == 500 - assert exc_info.value.context["cause_type"] == "_FakeAPIStatusError" - assert exc_info.value.context["provider_body"] == {"error": "download failed"} - assert exc_info.value.context["detail"] == "file_download_failed" - assert exc_info.value.retryable is True - - @pytest.mark.asyncio - async def test_write_wraps_runloop_http_error_with_provider_context( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - await session.start() - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - - async def _raise_upload_error(**kwargs: object) -> object: - _ = kwargs - raise _FakeAPIStatusError( - 429, - body={"error": "upload rate limited"}, - url=f"https://api.runloop.ai/v1/devboxes/{devbox.id}/files/project/output.txt", - method="PUT", - ) - - monkeypatch.setattr(devbox.file, "upload", _raise_upload_error) - - with pytest.raises(runloop_module.WorkspaceArchiveWriteError) as exc_info: - await session.write("/home/user/project/output.txt", io.BytesIO(b"hello")) - - assert exc_info.value.context["http_status"] == 429 - assert exc_info.value.context["cause_type"] == "_FakeAPIStatusError" - assert exc_info.value.context["provider_body"] == {"error": "upload rate limited"} - assert exc_info.value.context["detail"] == "file_upload_failed" - assert exc_info.value.retryable is True - - @pytest.mark.asyncio - async def test_manifest_apply_preserves_existing_files_in_non_empty_directory( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - manifest=Manifest( - root=f"{runloop_module.DEFAULT_RUNLOOP_WORKSPACE_ROOT}/project", - entries={"new.txt": File(content=b"new")}, - ), - options=runloop_module.RunloopSandboxClientOptions(), - ) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - devbox.files["project/existing.txt"] = b"keep" - - await session.start() - - assert devbox.files["project/existing.txt"] == b"keep" - assert devbox.files["project/new.txt"] == b"new" - - @pytest.mark.asyncio - async def test_persist_workspace_returns_native_snapshot_ref_and_hydrate_recreates_devbox( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - old_devbox_id = session.state.devbox_id - archive = await session.persist_workspace() - snapshot_id = runloop_module._decode_runloop_snapshot_ref(archive.read()) # noqa: SLF001 - await session.hydrate_workspace( - io.BytesIO(runloop_module._encode_runloop_snapshot_ref(snapshot_id="snap-1")) # noqa: SLF001 - ) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - - assert snapshot_id == "snap-1" - assert sdk.devbox.create_from_snapshot_calls == [ - ("snap-1", {"timeout": session.state.timeouts.resume_s}) - ] - assert session.state.devbox_id != old_devbox_id - - @pytest.mark.asyncio - async def test_restore_snapshot_on_resume_bypasses_workspace_clear( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions(), - ) - session.state.snapshot = _RestorableSnapshot( - id="runloop-snapshot", - payload=runloop_module._encode_runloop_snapshot_ref(snapshot_id="snap-9"), # noqa: SLF001 - ) - state = session.state - resumed = await client.resume(state) - inner = resumed._inner - - async def _unexpected_clear() -> None: - raise AssertionError("workspace clear should be bypassed for Runloop restore") - - inner._clear_workspace_root_on_resume = _unexpected_clear # noqa: SLF001 - await inner._restore_snapshot_into_workspace_on_resume() # noqa: SLF001 - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - - assert sdk.devbox.create_from_snapshot_calls == [ - ("snap-9", {"timeout": state.timeouts.resume_s}) - ] - - @pytest.mark.asyncio - async def test_restore_tar_snapshot_on_resume_clears_workspace_before_hydrate( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - manifest=Manifest(root=f"{runloop_module.DEFAULT_RUNLOOP_WORKSPACE_ROOT}/project"), - options=runloop_module.RunloopSandboxClientOptions(), - ) - session.state.snapshot = _RestorableSnapshot( - id="tar-snapshot", - payload=_build_tar_bytes({"new.txt": b"new"}), - ) - resumed = await client.resume(session.state) - inner = resumed._inner - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[resumed.state.devbox_id] - devbox.files["project/existing.txt"] = b"stale" - cleared = False - - async def _clear_workspace_root_on_resume() -> None: - nonlocal cleared - cleared = True - devbox.files.pop("project/existing.txt", None) - - inner._clear_workspace_root_on_resume = ( # noqa: SLF001 - _clear_workspace_root_on_resume - ) - await inner._restore_snapshot_into_workspace_on_resume() # noqa: SLF001 - - assert cleared is True - assert devbox.files["project/new.txt"] == b"new" - assert "project/existing.txt" not in devbox.files - - @pytest.mark.asyncio - async def test_restore_snapshot_on_resume_passes_dependencies_to_snapshot_restore( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - dependencies = Dependencies().bind_value("test.dep", object()) - - async with runloop_module.RunloopSandboxClient(dependencies=dependencies) as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - snapshot = _DependencyAwareSnapshot( - id="dep-aware-restore", - payload=runloop_module._encode_runloop_snapshot_ref(snapshot_id="snap-dep"), # noqa: SLF001 - ) - session.state.snapshot = snapshot - resumed = await client.resume(session.state) - - await resumed._inner._restore_snapshot_into_workspace_on_resume() # noqa: SLF001 - - assert snapshot.restore_dependencies - assert snapshot.restore_dependencies[-1] is not None - - @pytest.mark.asyncio - async def test_hydrate_workspace_wraps_provider_error_with_snapshot_context( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - - async def _raise_restore_error(snapshot_id: str, **kwargs: object) -> object: - _ = (snapshot_id, kwargs) - raise _FakeAPIStatusError( - 500, - body={"error": "restore failed"}, - url="https://api.runloop.ai/v1/devboxes/from_snapshot", - method="POST", - ) - - monkeypatch.setattr(sdk.devbox, "create_from_snapshot", _raise_restore_error) - - with pytest.raises(runloop_module.WorkspaceArchiveWriteError) as exc_info: - await session.hydrate_workspace( - io.BytesIO(runloop_module._encode_runloop_snapshot_ref(snapshot_id="snap-7")) # noqa: SLF001 - ) - - assert exc_info.value.context["reason"] == "snapshot_restore_failed" - assert exc_info.value.context["snapshot_id"] == "snap-7" - assert exc_info.value.context["http_status"] == 500 - assert exc_info.value.context["cause_type"] == "_FakeAPIStatusError" - assert exc_info.value.context["provider_body"] == {"error": "restore failed"} - - @pytest.mark.asyncio - async def test_hydrate_workspace_accepts_tar_fallback_payload( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - archive = _build_tar_bytes({"notes/output.txt": b"from tar"}) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - await session.hydrate_workspace(io.BytesIO(archive)) - payload = await session.read("/home/user/notes/output.txt") - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - - assert payload.read() == b"from tar" - assert f".sandbox-runloop-hydrate-{session.state.session_id.hex}.tar" not in devbox.files - - @pytest.mark.asyncio - async def test_hydrate_workspace_rejects_invalid_non_snapshot_non_tar_payload( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - - with pytest.raises(runloop_module.WorkspaceArchiveWriteError) as exc_info: - await session.hydrate_workspace(io.BytesIO(b"not-a-valid-tar")) - - assert exc_info.value.context["reason"] == "unsafe_or_invalid_tar" - - @pytest.mark.asyncio - async def test_persist_workspace_remounts_mounts_after_snapshot( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - mount = _RecordingMount() - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - manifest=Manifest( - root=runloop_module.DEFAULT_RUNLOOP_WORKSPACE_ROOT, - entries={"mount": mount}, - ), - options=runloop_module.RunloopSandboxClientOptions(), - ) - archive = await session.persist_workspace() - - assert runloop_module._decode_runloop_snapshot_ref(archive.read()) == "snap-1" # noqa: SLF001 - mount_path = Path(f"{runloop_module.DEFAULT_RUNLOOP_WORKSPACE_ROOT}/mount") - assert mount._unmounted_paths == [mount_path] - assert mount._mounted_paths == [mount_path] - - @pytest.mark.asyncio - async def test_resolve_exposed_port_wraps_provider_error_with_context( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions(exposed_ports=(4500,)) - ) - await session.start() - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - - async def _raise_tunnel_error(*args: object, **kwargs: object) -> str | None: - _ = (args, kwargs) - raise _FakeAPIStatusError( - 429, - body={"error": "tunnel rate limited"}, - url=f"https://api.runloop.ai/v1/devboxes/{devbox.id}", - method="GET", - ) - - monkeypatch.setattr(devbox, "get_tunnel_url", _raise_tunnel_error) - - with pytest.raises(runloop_module.ExposedPortUnavailableError) as exc_info: - await session.resolve_exposed_port(4500) - - assert exc_info.value.context["http_status"] == 429 - assert exc_info.value.context["cause_type"] == "_FakeAPIStatusError" - assert exc_info.value.context["provider_body"] == {"error": "tunnel rate limited"} - assert exc_info.value.context["detail"] == "get_tunnel_url_failed" - - @pytest.mark.asyncio - async def test_resolve_exposed_port_keeps_invalid_url_detail_for_parse_errors( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create( - options=runloop_module.RunloopSandboxClientOptions(exposed_ports=(4500,)) - ) - await session.start() - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - - async def _invalid_tunnel_url(*args: object, **kwargs: object) -> str | None: - _ = (args, kwargs) - return "https://" - - monkeypatch.setattr(devbox, "get_tunnel_url", _invalid_tunnel_url) - - with pytest.raises(runloop_module.ExposedPortUnavailableError) as exc_info: - await session.resolve_exposed_port(4500) - - assert exc_info.value.context["detail"] == "invalid_tunnel_url" - - @pytest.mark.asyncio - async def test_runloop_shell_capability_does_not_expose_write_stdin( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - capability = Shell() - capability.bind(session) - tools = capability.tools() - - assert [tool.name for tool in tools] == ["exec_command"] - - @pytest.mark.asyncio - async def test_exec_command_tool_uses_one_shot_exec_for_tty_requests( - self, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - runloop_module = _load_runloop_module(monkeypatch) - - async with runloop_module.RunloopSandboxClient() as client: - session = await client.create(options=runloop_module.RunloopSandboxClientOptions()) - await session.start() - sdk = _FakeAsyncRunloopSDK.created_instances[-1] - devbox = sdk.devbox.devboxes[session.state.devbox_id] - exec_calls_before = len(devbox.exec_calls) - exec_async_calls_before = len(devbox.exec_async_calls) - - output = await ExecCommandTool(session=session).run( - ExecCommandArgs(cmd="echo hello", tty=True, yield_time_ms=50) - ) - - assert "Process exited with code 0" in output - assert "Process running with session ID" not in output - assert "hello" in output - assert len(devbox.exec_calls) == exec_calls_before + 1 - assert len(devbox.exec_async_calls) == exec_async_calls_before diff --git a/tests/extensions/sandbox/test_runloop_capabilities_example.py b/tests/extensions/sandbox/test_runloop_capabilities_example.py deleted file mode 100644 index 75777cf144..0000000000 --- a/tests/extensions/sandbox/test_runloop_capabilities_example.py +++ /dev/null @@ -1,350 +0,0 @@ -from __future__ import annotations - -import importlib.util -import sys -import types -from pathlib import Path -from typing import Any, cast - -import pytest - - -def _load_example_module() -> Any: - path = ( - Path(__file__).resolve().parents[3] - / "examples" - / "sandbox" - / "extensions" - / "runloop" - / "capabilities.py" - ) - module_name = "tests.extensions.sandbox.runloop_capabilities_example" - spec = importlib.util.spec_from_file_location(module_name, path) - assert spec is not None - assert spec.loader is not None - module = importlib.util.module_from_spec(spec) - sys.modules[module_name] = module - spec.loader.exec_module(module) - return module - - -class _FakeNotFoundError(Exception): - def __init__(self) -> None: - self.status_code = 404 - self.response = types.SimpleNamespace(status_code=404) - - -class _FakeConflictError(Exception): - def __init__(self, message: str) -> None: - self.status_code = 400 - self.response = types.SimpleNamespace(status_code=400) - self.body = {"message": message} - - -class _FakeSecret: - def __init__(self, name: str, secret_id: str) -> None: - self.id = secret_id - self.name = name - - -class _FakeSecretsClient: - def __init__(self) -> None: - self.secrets: dict[str, _FakeSecret] = {} - self.create_calls: list[tuple[str, str]] = [] - self.delete_calls: list[str] = [] - self._counter = 0 - - def add(self, name: str) -> _FakeSecret: - self._counter += 1 - secret = _FakeSecret(name=name, secret_id=f"secret-{self._counter}") - self.secrets[name] = secret - return secret - - async def get(self, name: str) -> _FakeSecret: - if name not in self.secrets: - raise _FakeNotFoundError() - return self.secrets[name] - - async def create(self, *, name: str, value: str) -> _FakeSecret: - self.create_calls.append((name, value)) - return self.add(name) - - -class _FakePolicy: - def __init__(self, policy_id: str, name: str, description: str | None = None) -> None: - self.id = policy_id - self.name = name - self.description = description - - -class _FakePolicyRef: - def __init__(self, policy: _FakePolicy) -> None: - self._policy = policy - - async def get_info(self) -> object: - return types.SimpleNamespace( - id=self._policy.id, - name=self._policy.name, - description=self._policy.description, - ) - - -class _FakePolicyListItem: - def __init__(self, policy_id: str) -> None: - self.id = policy_id - - -class _FakeNetworkPoliciesClient: - def __init__(self) -> None: - self.policies: dict[str, _FakePolicy] = {} - self.create_calls: list[dict[str, object]] = [] - self.delete_calls: list[str] = [] - self._counter = 0 - - def add(self, name: str, description: str | None = None) -> _FakePolicy: - self._counter += 1 - policy = _FakePolicy( - policy_id=f"np-{self._counter}", - name=name, - description=description, - ) - self.policies[policy.id] = policy - return policy - - async def list(self, **params: object) -> list[_FakePolicyListItem]: - name = params.get("name") - policies = list(self.policies.values()) - if isinstance(name, str): - policies = [policy for policy in policies if policy.name == name] - return [_FakePolicyListItem(policy.id) for policy in policies] - - async def create(self, **params: object) -> _FakePolicy: - self.create_calls.append(dict(params)) - name = str(params["name"]) - if any(policy.name == name for policy in self.policies.values()): - raise _FakeConflictError(f"NetworkPolicy with name '{name}' already exists") - description = cast( - str | None, - params.get("description") if isinstance(params.get("description"), str) else None, - ) - return self.add( - name=name, - description=description, - ) - - def get(self, policy_id: str) -> _FakePolicyRef: - return _FakePolicyRef(self.policies[policy_id]) - - -class _FakePlatformClient: - def __init__(self) -> None: - self.secrets = _FakeSecretsClient() - self.network_policies = _FakeNetworkPoliciesClient() - - -class _FakeRunloopClient: - def __init__(self) -> None: - self.platform = _FakePlatformClient() - - -@pytest.mark.asyncio -async def test_query_runloop_secret_returns_non_sensitive_metadata() -> None: - module = _load_example_module() - client = _FakeRunloopClient() - secret = client.platform.secrets.add("RUNLOOP_CAPABILITIES_EXAMPLE_TOKEN") - - result = await module._query_runloop_secret( # noqa: SLF001 - client, - name=secret.name, - ) - - assert result.found is True - assert result.id == secret.id - assert "value" not in result.model_dump(mode="json") - - -@pytest.mark.asyncio -async def test_query_runloop_secret_reports_missing_before_create() -> None: - module = _load_example_module() - client = _FakeRunloopClient() - - result = await module._query_runloop_secret( # noqa: SLF001 - client, - name="RUNLOOP_CAPABILITIES_EXAMPLE_TOKEN", - ) - - assert result.found is False - assert result.id is None - - -@pytest.mark.asyncio -async def test_query_runloop_network_policy_reports_existing_resource() -> None: - module = _load_example_module() - client = _FakeRunloopClient() - policy = client.platform.network_policies.add( - "runloop-capabilities-example-policy", - description="Persistent example policy.", - ) - - result = await module._query_runloop_network_policy( # noqa: SLF001 - client, - name=policy.name, - ) - - assert result.found is True - assert result.id == policy.id - assert result.description == "Persistent example policy." - - -@pytest.mark.asyncio -async def test_bootstrap_persistent_resources_reuses_existing_resources_without_cleanup() -> None: - module = _load_example_module() - client = _FakeRunloopClient() - secret = client.platform.secrets.add("RUNLOOP_CAPABILITIES_EXAMPLE_TOKEN") - policy = client.platform.network_policies.add("runloop-capabilities-example-policy") - query_results = { - "secret": module.RunloopResourceQueryResult( - resource_type="secret", - name=secret.name, - found=True, - id=secret.id, - ), - "network_policy": module.RunloopResourceQueryResult( - resource_type="network_policy", - name=policy.name, - found=True, - id=policy.id, - ), - } - - bootstrap = await module._bootstrap_persistent_resources( # noqa: SLF001 - client, - managed_secret_name=secret.name, - managed_secret_value="runloop-capabilities-example-token", - network_policy_name=policy.name, - network_policy_id_override=None, - query_results=query_results, - axon_name=None, - ) - - secret_bootstrap = bootstrap["secret"] - network_policy_bootstrap = bootstrap["network_policy"] - assert secret_bootstrap.action == "reused" - assert network_policy_bootstrap.action == "reused" - assert client.platform.secrets.create_calls == [] - assert client.platform.network_policies.create_calls == [] - assert client.platform.secrets.delete_calls == [] - assert client.platform.network_policies.delete_calls == [] - - -@pytest.mark.asyncio -async def test_bootstrap_persistent_resources_creates_missing_resources() -> None: - module = _load_example_module() - client = _FakeRunloopClient() - query_results = { - "secret": module.RunloopResourceQueryResult( - resource_type="secret", - name="RUNLOOP_CAPABILITIES_EXAMPLE_TOKEN", - found=False, - ), - "network_policy": module.RunloopResourceQueryResult( - resource_type="network_policy", - name="runloop-capabilities-example-policy", - found=False, - ), - } - - bootstrap = await module._bootstrap_persistent_resources( # noqa: SLF001 - client, - managed_secret_name="RUNLOOP_CAPABILITIES_EXAMPLE_TOKEN", - managed_secret_value="runloop-capabilities-example-token", - network_policy_name="runloop-capabilities-example-policy", - network_policy_id_override=None, - query_results=query_results, - axon_name=None, - ) - - secret_bootstrap = bootstrap["secret"] - network_policy_bootstrap = bootstrap["network_policy"] - assert secret_bootstrap.action == "created" - assert network_policy_bootstrap.action == "created" - assert client.platform.secrets.create_calls == [ - ("RUNLOOP_CAPABILITIES_EXAMPLE_TOKEN", "runloop-capabilities-example-token") - ] - assert client.platform.network_policies.create_calls == [ - { - "name": "runloop-capabilities-example-policy", - "allow_all": True, - "description": "Persistent network policy for the Runloop capabilities example.", - } - ] - - -@pytest.mark.asyncio -async def test_bootstrap_persistent_resources_respects_policy_override() -> None: - module = _load_example_module() - client = _FakeRunloopClient() - query_results = { - "secret": module.RunloopResourceQueryResult( - resource_type="secret", - name="RUNLOOP_CAPABILITIES_EXAMPLE_TOKEN", - found=False, - ), - "network_policy": module.RunloopResourceQueryResult( - resource_type="network_policy", - name="runloop-capabilities-example-policy", - found=False, - ), - } - - bootstrap = await module._bootstrap_persistent_resources( # noqa: SLF001 - client, - managed_secret_name="RUNLOOP_CAPABILITIES_EXAMPLE_TOKEN", - managed_secret_value="runloop-capabilities-example-token", - network_policy_name="runloop-capabilities-example-policy", - network_policy_id_override="np-override", - query_results=query_results, - axon_name=None, - ) - - network_policy_bootstrap = bootstrap["network_policy"] - assert network_policy_bootstrap.action == "override" - assert network_policy_bootstrap.id == "np-override" - assert client.platform.network_policies.create_calls == [] - - -@pytest.mark.asyncio -async def test_bootstrap_persistent_resources_recovers_from_existing_policy_conflict() -> None: - module = _load_example_module() - client = _FakeRunloopClient() - policy = client.platform.network_policies.add( - "runloop-capabilities-example-policy", - description="Persistent example policy.", - ) - query_results = { - "secret": module.RunloopResourceQueryResult( - resource_type="secret", - name="RUNLOOP_CAPABILITIES_EXAMPLE_TOKEN", - found=False, - ), - "network_policy": module.RunloopResourceQueryResult( - resource_type="network_policy", - name=policy.name, - found=False, - ), - } - - bootstrap = await module._bootstrap_persistent_resources( # noqa: SLF001 - client, - managed_secret_name="RUNLOOP_CAPABILITIES_EXAMPLE_TOKEN", - managed_secret_value="runloop-capabilities-example-token", - network_policy_name=policy.name, - network_policy_id_override=None, - query_results=query_results, - axon_name=None, - ) - - network_policy_bootstrap = bootstrap["network_policy"] - assert network_policy_bootstrap.action == "reused" - assert network_policy_bootstrap.found_before_bootstrap is True - assert network_policy_bootstrap.id == policy.id diff --git a/tests/extensions/sandbox/test_runloop_mounts.py b/tests/extensions/sandbox/test_runloop_mounts.py deleted file mode 100644 index 8ee619fda9..0000000000 --- a/tests/extensions/sandbox/test_runloop_mounts.py +++ /dev/null @@ -1,226 +0,0 @@ -from __future__ import annotations - -import io -import types -import uuid -from pathlib import Path -from typing import Any, cast - -import pytest - -from agents.sandbox import Manifest -from agents.sandbox.entries import RcloneMountPattern, S3Mount -from agents.sandbox.errors import MountConfigError -from agents.sandbox.session.base_sandbox_session import BaseSandboxSession -from agents.sandbox.types import ExecResult - - -class _FakeRunloopMountSession(BaseSandboxSession): - def __init__(self, results: list[ExecResult] | None = None) -> None: - self.state = cast( - Any, - types.SimpleNamespace( - session_id=uuid.uuid4(), - manifest=Manifest(root="/workspace"), - ), - ) - self._results = list(results or []) - self.exec_calls: list[str] = [] - - async def _exec_internal( - self, - *command: str | Path, - timeout: float | None = None, - ) -> ExecResult: - _ = timeout - cmd_str = " ".join(str(c) for c in command) - self.exec_calls.append(cmd_str) - if self._results: - return self._results.pop(0) - return ExecResult(stdout=b"", stderr=b"", exit_code=0) - - async def read(self, path: Path, *, user: object = None) -> io.IOBase: - _ = (path, user) - return io.BytesIO(b"") - - async def write(self, path: Path, data: io.IOBase, *, user: object = None) -> None: - _ = (path, data, user) - - async def persist_workspace(self) -> io.IOBase: - raise AssertionError("not expected") - - async def hydrate_workspace(self, data: io.IOBase) -> None: - _ = data - raise AssertionError("not expected") - - async def running(self) -> bool: - return True - - -_FakeRunloopMountSession.__name__ = "RunloopSandboxSession" - - -def _exec_ok(stdout: bytes = b"") -> ExecResult: - return ExecResult(stdout=stdout, stderr=b"", exit_code=0) - - -def _exec_fail() -> ExecResult: - return ExecResult(stdout=b"", stderr=b"", exit_code=1) - - -def test_runloop_package_re_exports_cloud_bucket_strategy() -> None: - package_module = __import__( - "agents.extensions.sandbox.runloop", - fromlist=["RunloopCloudBucketMountStrategy"], - ) - - assert hasattr(package_module, "RunloopCloudBucketMountStrategy") - - -def test_runloop_extension_re_exports_cloud_bucket_strategy() -> None: - package_module = __import__( - "agents.extensions.sandbox", - fromlist=["RunloopCloudBucketMountStrategy"], - ) - - assert hasattr(package_module, "RunloopCloudBucketMountStrategy") - - -def test_runloop_mount_strategy_type_and_default_pattern() -> None: - from agents.extensions.sandbox.runloop.mounts import RunloopCloudBucketMountStrategy - - strategy = RunloopCloudBucketMountStrategy() - - assert strategy.type == "runloop_cloud_bucket" - assert isinstance(strategy.pattern, RcloneMountPattern) - assert strategy.pattern.mode == "fuse" - - -def test_runloop_mount_strategy_round_trips_through_manifest() -> None: - from agents.extensions.sandbox.runloop.mounts import RunloopCloudBucketMountStrategy - - manifest = Manifest.model_validate( - { - "root": "/workspace", - "entries": { - "bucket": { - "type": "s3_mount", - "bucket": "my-bucket", - "mount_strategy": {"type": "runloop_cloud_bucket"}, - } - }, - } - ) - - mount = manifest.entries["bucket"] - assert isinstance(mount, S3Mount) - assert isinstance(mount.mount_strategy, RunloopCloudBucketMountStrategy) - - -def test_runloop_session_guard_rejects_wrong_type() -> None: - from agents.extensions.sandbox.runloop.mounts import _assert_runloop_session - - class _WrongSession: - pass - - with pytest.raises(MountConfigError, match="RunloopSandboxSession"): - _assert_runloop_session(_WrongSession()) # type: ignore[arg-type] - - -def test_runloop_session_guard_accepts_correct_type() -> None: - from agents.extensions.sandbox.runloop.mounts import _assert_runloop_session - - _assert_runloop_session(_FakeRunloopMountSession()) - - -@pytest.mark.asyncio -async def test_runloop_ensure_rclone_installs_verified_release() -> None: - from agents.extensions.sandbox._rclone import _RCLONE_VERSION, ensure_rclone - - session = _FakeRunloopMountSession( - [ - _exec_fail(), - _exec_ok(), - _exec_ok(stdout=b"aarch64\n"), - _exec_ok(), - _exec_ok(), - _exec_ok(), - _exec_ok(), - ] - ) - - await ensure_rclone(session) - - assert session.exec_calls[:2] == [ - "sh -lc command -v rclone >/dev/null 2>&1 || test -x /usr/local/bin/rclone", - "sh -lc command -v apt-get >/dev/null 2>&1", - ] - assert session.exec_calls[2] == "uname -m" - assert session.exec_calls[3] == ( - "sudo -u root -- sh -lc DEBIAN_FRONTEND=noninteractive " - "DEBCONF_NOWARNINGS=yes apt-get -o Dpkg::Use-Pty=0 update -qq" - ) - assert session.exec_calls[4] == ( - "sudo -u root -- sh -lc DEBIAN_FRONTEND=noninteractive " - "DEBCONF_NOWARNINGS=yes apt-get -o Dpkg::Use-Pty=0 install -y -qq " - "ca-certificates coreutils curl unzip" - ) - assert session.exec_calls[5].startswith("sudo -u root -- sh -lc set -eu\n") - assert f"rclone-v{_RCLONE_VERSION}-linux-arm64.zip" in session.exec_calls[5] - assert "sha256sum --check --strict -" in session.exec_calls[5] - assert session.exec_calls[6] == ( - "sh -lc command -v rclone >/dev/null 2>&1 || test -x /usr/local/bin/rclone" - ) - - -@pytest.mark.asyncio -async def test_runloop_ensure_fuse_installs_missing_fusermount() -> None: - from agents.extensions.sandbox.runloop.mounts import _ensure_fuse_support - - session = _FakeRunloopMountSession( - [ - _exec_ok(), - _exec_ok(), - _exec_fail(), - _exec_ok(), - _exec_ok(), - _exec_ok(), - _exec_ok(), - _exec_ok(), - ] - ) - - await _ensure_fuse_support(session) - - assert session.exec_calls == [ - "sh -lc test -c /dev/fuse", - "sh -lc grep -qw fuse /proc/filesystems", - "sh -lc command -v fusermount3 >/dev/null 2>&1 || command -v fusermount >/dev/null 2>&1", - "sh -lc command -v apt-get >/dev/null 2>&1", - ( - "sudo -u root -- sh -lc DEBIAN_FRONTEND=noninteractive " - "DEBCONF_NOWARNINGS=yes apt-get -o Dpkg::Use-Pty=0 update -qq" - ), - ( - "sudo -u root -- sh -lc DEBIAN_FRONTEND=noninteractive " - "DEBCONF_NOWARNINGS=yes apt-get -o Dpkg::Use-Pty=0 install -y -qq fuse3" - ), - "sh -lc command -v fusermount3 >/dev/null 2>&1 || command -v fusermount >/dev/null 2>&1", - ( - "sudo -u root -- sh -lc chmod a+rw /dev/fuse && " - "touch /etc/fuse.conf && " - "(grep -qxF user_allow_other /etc/fuse.conf || " - "printf '\\nuser_allow_other\\n' >> /etc/fuse.conf)" - ), - ] - - -@pytest.mark.asyncio -async def test_runloop_rclone_pattern_adds_fuse_access_args() -> None: - from agents.extensions.sandbox._rclone import rclone_pattern_for_session - - session = _FakeRunloopMountSession([_exec_ok(stdout=b"1000\n1000\n")]) - - pattern = await rclone_pattern_for_session(session, RcloneMountPattern(mode="fuse")) - - assert pattern.extra_args == ["--allow-other", "--uid", "1000", "--gid", "1000"] diff --git a/tests/fixtures/released_api_contract.json b/tests/fixtures/released_api_contract.json index 268af7ece3..4403182ac9 100644 --- a/tests/fixtures/released_api_contract.json +++ b/tests/fixtures/released_api_contract.json @@ -18375,2446 +18375,6 @@ } ] }, - "agents.extensions.sandbox.RunloopAfterIdle": { - "dataclass_fields": [], - "kind": "class", - "members": {}, - "model_fields": [ - { - "default": { - "kind": "required" - }, - "name": "idle_time_seconds" - }, - { - "default": { - "kind": "required" - }, - "name": "on_idle" - } - ], - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "VAR_KEYWORD", - "name": "data" - } - ] - }, - "agents.extensions.sandbox.RunloopCloudBucketMountStrategy": { - "dataclass_fields": [], - "kind": "class", - "members": { - "activate": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "mount" - }, - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "session" - }, - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "dest" - }, - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "base_dir" - } - ] - }, - "build_docker_volume_driver_config": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "mount" - } - ] - }, - "deactivate": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "mount" - }, - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "session" - }, - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "dest" - }, - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "base_dir" - } - ] - }, - "parse": { - "binding": "class", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "payload" - } - ] - }, - "restore_after_snapshot": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "mount" - }, - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "session" - }, - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "path" - } - ] - }, - "supports_native_snapshot_detach": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "mount" - } - ] - }, - "teardown_for_snapshot": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "mount" - }, - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "session" - }, - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "path" - } - ] - }, - "validate_mount": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "mount" - } - ] - } - }, - "model_fields": [ - { - "default": { - "kind": "literal", - "type": "builtins.str", - "value": "runloop_cloud_bucket" - }, - "name": "type" - }, - { - "default": { - "kind": "model", - "type": "agents.sandbox.entries.mounts.patterns.RcloneMountPattern", - "value": { - "items": [ - [ - { - "kind": "literal", - "type": "builtins.str", - "value": "type" - }, - { - "kind": "literal", - "type": "builtins.str", - "value": "rclone" - } - ], - [ - { - "kind": "literal", - "type": "builtins.str", - "value": "mode" - }, - { - "kind": "literal", - "type": "builtins.str", - "value": "fuse" - } - ], - [ - { - "kind": "literal", - "type": "builtins.str", - "value": "remote_name" - }, - { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - } - ], - [ - { - "kind": "literal", - "type": "builtins.str", - "value": "extra_args" - }, - { - "items": [], - "kind": "sequence", - "type": "builtins.list" - } - ], - [ - { - "kind": "literal", - "type": "builtins.str", - "value": "nfs_addr" - }, - { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - } - ], - [ - { - "kind": "literal", - "type": "builtins.str", - "value": "nfs_mount_options" - }, - { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - } - ], - [ - { - "kind": "literal", - "type": "builtins.str", - "value": "config_file_path" - }, - { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - } - ] - ], - "kind": "mapping", - "type": "builtins.dict" - } - }, - "name": "pattern" - } - ], - "parameters": [ - { - "default": { - "kind": "literal", - "type": "builtins.str", - "value": "runloop_cloud_bucket" - }, - "kind": "KEYWORD_ONLY", - "name": "type" - }, - { - "default": { - "kind": "model", - "type": "agents.sandbox.entries.mounts.patterns.RcloneMountPattern", - "value": { - "items": [ - [ - { - "kind": "literal", - "type": "builtins.str", - "value": "type" - }, - { - "kind": "literal", - "type": "builtins.str", - "value": "rclone" - } - ], - [ - { - "kind": "literal", - "type": "builtins.str", - "value": "mode" - }, - { - "kind": "literal", - "type": "builtins.str", - "value": "fuse" - } - ], - [ - { - "kind": "literal", - "type": "builtins.str", - "value": "remote_name" - }, - { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - } - ], - [ - { - "kind": "literal", - "type": "builtins.str", - "value": "extra_args" - }, - { - "items": [], - "kind": "sequence", - "type": "builtins.list" - } - ], - [ - { - "kind": "literal", - "type": "builtins.str", - "value": "nfs_addr" - }, - { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - } - ], - [ - { - "kind": "literal", - "type": "builtins.str", - "value": "nfs_mount_options" - }, - { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - } - ], - [ - { - "kind": "literal", - "type": "builtins.str", - "value": "config_file_path" - }, - { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - } - ] - ], - "kind": "mapping", - "type": "builtins.dict" - } - }, - "kind": "KEYWORD_ONLY", - "name": "pattern" - } - ] - }, - "agents.extensions.sandbox.RunloopExistingSecret": { - "dataclass_fields": [], - "kind": "class", - "members": {}, - "model_fields": [], - "parameters": [] - }, - "agents.extensions.sandbox.RunloopGatewaySpec": { - "dataclass_fields": [], - "kind": "class", - "members": {}, - "model_fields": [ - { - "default": { - "kind": "required" - }, - "name": "gateway" - }, - { - "default": { - "kind": "required" - }, - "name": "secret" - } - ], - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "gateway" - }, - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "secret" - } - ] - }, - "agents.extensions.sandbox.RunloopLaunchParameters": { - "dataclass_fields": [], - "kind": "class", - "members": {}, - "model_fields": [ - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "after_idle" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "architecture" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "available_ports" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "custom_cpu_cores" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "custom_disk_size" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "custom_gb_memory" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "keep_alive_time_seconds" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "launch_commands" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "lifecycle" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "network_policy_id" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "provisioning_tier" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "required_services" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "resource_size_request" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "user_parameters" - } - ], - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "VAR_KEYWORD", - "name": "data" - } - ] - }, - "agents.extensions.sandbox.RunloopMcpSpec": { - "dataclass_fields": [], - "kind": "class", - "members": {}, - "model_fields": [ - { - "default": { - "kind": "required" - }, - "name": "mcp_config" - }, - { - "default": { - "kind": "required" - }, - "name": "secret" - } - ], - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "mcp_config" - }, - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "secret" - } - ] - }, - "agents.extensions.sandbox.RunloopPlatformClient": { - "dataclass_fields": [], - "kind": "class", - "members": {}, - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "_sdk" - } - ] - }, - "agents.extensions.sandbox.RunloopSandboxClient": { - "dataclass_fields": [], - "kind": "class", - "members": { - "close": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [] - }, - "create": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "snapshot" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "manifest" - }, - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "options" - } - ] - }, - "delete": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "session" - } - ] - }, - "deserialize_session_state": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "payload" - } - ] - }, - "resume": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "state" - } - ] - }, - "serialize_session_state": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "state" - } - ] - } - }, - "parameters": [ - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "bearer_token" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "base_url" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "instrumentation" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "dependencies" - } - ] - }, - "agents.extensions.sandbox.RunloopSandboxClientOptions": { - "dataclass_fields": [], - "kind": "class", - "members": { - "parse": { - "binding": "class", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "payload" - } - ] - } - }, - "model_fields": [ - { - "default": { - "kind": "literal", - "type": "builtins.str", - "value": "runloop" - }, - "name": "type" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "blueprint_id" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "blueprint_name" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "env_vars" - }, - { - "default": { - "kind": "literal", - "type": "builtins.bool", - "value": false - }, - "name": "pause_on_exit" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "name" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "timeouts" - }, - { - "default": { - "items": [], - "kind": "sequence", - "type": "builtins.tuple" - }, - "name": "exposed_ports" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "user_parameters" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "launch_parameters" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "tunnel" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "gateways" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "mcp" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "metadata" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "managed_secrets" - } - ], - "parameters": [ - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "blueprint_id" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "blueprint_name" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "env_vars" - }, - { - "default": { - "kind": "literal", - "type": "builtins.bool", - "value": false - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "pause_on_exit" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "name" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "timeouts" - }, - { - "default": { - "items": [], - "kind": "sequence", - "type": "builtins.tuple" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "exposed_ports" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "user_parameters" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "launch_parameters" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "tunnel" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "gateways" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "mcp" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "metadata" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "managed_secrets" - }, - { - "default": { - "kind": "literal", - "type": "builtins.str", - "value": "runloop" - }, - "kind": "KEYWORD_ONLY", - "name": "type" - } - ] - }, - "agents.extensions.sandbox.RunloopSandboxSession": { - "dataclass_fields": [], - "kind": "class", - "members": { - "aclose": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [] - }, - "apply_manifest": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "literal", - "type": "builtins.bool", - "value": false - }, - "kind": "KEYWORD_ONLY", - "name": "only_ephemeral" - } - ] - }, - "apply_patch": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "operations" - }, - { - "default": { - "kind": "literal", - "type": "builtins.str", - "value": "v4a" - }, - "kind": "KEYWORD_ONLY", - "name": "patch_format" - } - ] - }, - "describe": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [] - }, - "exec": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "VAR_POSITIONAL", - "name": "command" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "timeout" - }, - { - "default": { - "kind": "literal", - "type": "builtins.bool", - "value": true - }, - "kind": "KEYWORD_ONLY", - "name": "shell" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "user" - } - ] - }, - "extract": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "path" - }, - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "data" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "compression_scheme" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "archive_limits" - } - ] - }, - "from_state": { - "binding": "class", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "state" - }, - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "sdk" - }, - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "devbox" - } - ] - }, - "hydrate_workspace": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "data" - } - ] - }, - "ls": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "path" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "user" - } - ] - }, - "mkdir": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "path" - }, - { - "default": { - "kind": "literal", - "type": "builtins.bool", - "value": false - }, - "kind": "KEYWORD_ONLY", - "name": "parents" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "user" - } - ] - }, - "mv": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "source" - }, - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "destination" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "user" - } - ] - }, - "normalize_path": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "path" - }, - { - "default": { - "kind": "literal", - "type": "builtins.bool", - "value": false - }, - "kind": "KEYWORD_ONLY", - "name": "for_write" - } - ] - }, - "persist_workspace": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [] - }, - "provision_manifest_accounts": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [] - }, - "pty_exec_start": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "VAR_POSITIONAL", - "name": "command" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "timeout" - }, - { - "default": { - "kind": "literal", - "type": "builtins.bool", - "value": true - }, - "kind": "KEYWORD_ONLY", - "name": "shell" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "user" - }, - { - "default": { - "kind": "literal", - "type": "builtins.bool", - "value": false - }, - "kind": "KEYWORD_ONLY", - "name": "tty" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "yield_time_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "max_output_tokens" - } - ] - }, - "pty_terminate_all": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [] - }, - "pty_write_stdin": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "session_id" - }, - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "chars" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "yield_time_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "max_output_tokens" - } - ] - }, - "read": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "path" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "user" - } - ] - }, - "read_bounded": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "path" - }, - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "max_bytes" - } - ] - }, - "register_persist_workspace_skip_path": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "path" - } - ] - }, - "register_pre_stop_hook": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "hook" - } - ] - }, - "resolve_exposed_port": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "port" - } - ] - }, - "rm": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "path" - }, - { - "default": { - "kind": "literal", - "type": "builtins.bool", - "value": false - }, - "kind": "KEYWORD_ONLY", - "name": "recursive" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "user" - } - ] - }, - "run_pre_stop_hooks": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [] - }, - "running": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [] - }, - "same_file": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "left" - }, - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "right" - }, - { - "default": { - "kind": "literal", - "type": "builtins.bool", - "value": true - }, - "kind": "KEYWORD_ONLY", - "name": "follow_symlinks" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "user" - } - ] - }, - "set_dependencies": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "dependencies" - } - ] - }, - "should_provision_manifest_accounts_on_resume": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [] - }, - "shutdown": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [] - }, - "start": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [] - }, - "stop": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [] - }, - "supports_docker_volume_mounts": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [] - }, - "supports_pty": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [] - }, - "write": { - "binding": "instance", - "execution_kind": "coroutine", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "path" - }, - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "data" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "user" - } - ] - } - }, - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "state" - }, - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "sdk" - }, - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "devbox" - } - ] - }, - "agents.extensions.sandbox.RunloopSandboxSessionState": { - "dataclass_fields": [], - "kind": "class", - "members": { - "assert_path_grants_rebound": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [] - }, - "model_post_init": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_ONLY", - "name": "context" - } - ] - }, - "model_validate_json": { - "binding": "class", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "json_data" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "strict" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "extra" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "context" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "by_alias" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "by_name" - } - ] - }, - "parse": { - "binding": "class", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "payload" - } - ] - }, - "rebind_persisted_mount_authority": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "trusted_manifest" - }, - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "provider_backend_id" - } - ] - }, - "rebind_persisted_path_grants": { - "binding": "instance", - "execution_kind": "sync", - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "POSITIONAL_OR_KEYWORD", - "name": "trusted_manifest" - } - ] - } - }, - "model_fields": [ - { - "default": { - "kind": "literal", - "type": "builtins.str", - "value": "runloop" - }, - "name": "type" - }, - { - "default": { - "factory": "uuid.uuid4", - "kind": "factory" - }, - "name": "session_id" - }, - { - "default": { - "kind": "required" - }, - "name": "snapshot" - }, - { - "default": { - "kind": "required" - }, - "name": "manifest" - }, - { - "default": { - "factory": "builtins.tuple", - "kind": "factory" - }, - "name": "exposed_ports" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "snapshot_fingerprint" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "snapshot_fingerprint_version" - }, - { - "default": { - "kind": "literal", - "type": "builtins.bool", - "value": false - }, - "name": "workspace_root_ready" - }, - { - "default": { - "kind": "required" - }, - "name": "devbox_id" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "blueprint_id" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "blueprint_name" - }, - { - "default": { - "factory": "builtins.dict", - "kind": "factory" - }, - "name": "base_env_vars" - }, - { - "default": { - "kind": "literal", - "type": "builtins.bool", - "value": false - }, - "name": "pause_on_exit" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "name" - }, - { - "default": { - "factory": "agents.extensions.sandbox.runloop.sandbox.RunloopTimeouts", - "kind": "factory" - }, - "name": "timeouts" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "user_parameters" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "launch_parameters" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "tunnel" - }, - { - "default": { - "factory": "builtins.dict", - "kind": "factory" - }, - "name": "gateways" - }, - { - "default": { - "factory": "builtins.dict", - "kind": "factory" - }, - "name": "mcp" - }, - { - "default": { - "factory": "builtins.dict", - "kind": "factory" - }, - "name": "metadata" - }, - { - "default": { - "factory": "builtins.dict", - "kind": "factory" - }, - "name": "secret_refs" - } - ], - "parameters": [ - { - "default": { - "kind": "literal", - "type": "builtins.str", - "value": "runloop" - }, - "kind": "KEYWORD_ONLY", - "name": "type" - }, - { - "default": { - "kind": "factory" - }, - "kind": "KEYWORD_ONLY", - "name": "session_id" - }, - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "snapshot" - }, - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "manifest" - }, - { - "default": { - "kind": "factory" - }, - "kind": "KEYWORD_ONLY", - "name": "exposed_ports" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "snapshot_fingerprint" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "snapshot_fingerprint_version" - }, - { - "default": { - "kind": "literal", - "type": "builtins.bool", - "value": false - }, - "kind": "KEYWORD_ONLY", - "name": "workspace_root_ready" - }, - { - "default": { - "kind": "required" - }, - "kind": "KEYWORD_ONLY", - "name": "devbox_id" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "blueprint_id" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "blueprint_name" - }, - { - "default": { - "kind": "factory" - }, - "kind": "KEYWORD_ONLY", - "name": "base_env_vars" - }, - { - "default": { - "kind": "literal", - "type": "builtins.bool", - "value": false - }, - "kind": "KEYWORD_ONLY", - "name": "pause_on_exit" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "name" - }, - { - "default": { - "kind": "factory" - }, - "kind": "KEYWORD_ONLY", - "name": "timeouts" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "user_parameters" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "launch_parameters" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "tunnel" - }, - { - "default": { - "kind": "factory" - }, - "kind": "KEYWORD_ONLY", - "name": "gateways" - }, - { - "default": { - "kind": "factory" - }, - "kind": "KEYWORD_ONLY", - "name": "mcp" - }, - { - "default": { - "kind": "factory" - }, - "kind": "KEYWORD_ONLY", - "name": "metadata" - }, - { - "default": { - "kind": "factory" - }, - "kind": "KEYWORD_ONLY", - "name": "secret_refs" - } - ] - }, - "agents.extensions.sandbox.RunloopTimeouts": { - "dataclass_fields": [], - "kind": "class", - "members": {}, - "model_fields": [ - { - "default": { - "kind": "literal", - "type": "builtins.int", - "value": 86400 - }, - "name": "exec_timeout_unbounded_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 300.0 - }, - "name": "create_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 10.0 - }, - "name": "keepalive_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 30.0 - }, - "name": "cleanup_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 30.0 - }, - "name": "fast_op_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 1800.0 - }, - "name": "file_upload_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 1800.0 - }, - "name": "file_download_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 300.0 - }, - "name": "snapshot_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 120.0 - }, - "name": "suspend_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 300.0 - }, - "name": "resume_s" - } - ], - "parameters": [ - { - "default": { - "kind": "literal", - "type": "builtins.int", - "value": 86400 - }, - "kind": "KEYWORD_ONLY", - "name": "exec_timeout_unbounded_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 300.0 - }, - "kind": "KEYWORD_ONLY", - "name": "create_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 10.0 - }, - "kind": "KEYWORD_ONLY", - "name": "keepalive_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 30.0 - }, - "kind": "KEYWORD_ONLY", - "name": "cleanup_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 30.0 - }, - "kind": "KEYWORD_ONLY", - "name": "fast_op_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 1800.0 - }, - "kind": "KEYWORD_ONLY", - "name": "file_upload_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 1800.0 - }, - "kind": "KEYWORD_ONLY", - "name": "file_download_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 300.0 - }, - "kind": "KEYWORD_ONLY", - "name": "snapshot_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 120.0 - }, - "kind": "KEYWORD_ONLY", - "name": "suspend_s" - }, - { - "default": { - "kind": "literal", - "type": "builtins.float", - "value": 300.0 - }, - "kind": "KEYWORD_ONLY", - "name": "resume_s" - } - ] - }, - "agents.extensions.sandbox.RunloopTunnelConfig": { - "dataclass_fields": [], - "kind": "class", - "members": {}, - "model_fields": [ - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "auth_mode" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "http_keep_alive" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "name": "wake_on_http" - } - ], - "parameters": [ - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "auth_mode" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "http_keep_alive" - }, - { - "default": { - "kind": "literal", - "type": "builtins.NoneType", - "value": null - }, - "kind": "KEYWORD_ONLY", - "name": "wake_on_http" - } - ] - }, - "agents.extensions.sandbox.RunloopUserParameters": { - "dataclass_fields": [], - "kind": "class", - "members": {}, - "model_fields": [ - { - "default": { - "kind": "required" - }, - "name": "uid" - }, - { - "default": { - "kind": "required" - }, - "name": "username" - } - ], - "parameters": [ - { - "default": { - "kind": "required" - }, - "kind": "VAR_KEYWORD", - "name": "data" - } - ] - }, "agents.extensions.sandbox.VercelCloudBucketMountStrategy": { "dataclass_fields": [], "kind": "class", @@ -46630,54 +44190,6 @@ "module": "agents.extensions.sandbox", "name": "ModalSandboxClientOptions" }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopAfterIdle", - "module": "agents.extensions.sandbox", - "name": "RunloopAfterIdle" - }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopGatewaySpec", - "module": "agents.extensions.sandbox", - "name": "RunloopGatewaySpec" - }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopLaunchParameters", - "module": "agents.extensions.sandbox", - "name": "RunloopLaunchParameters" - }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopMcpSpec", - "module": "agents.extensions.sandbox", - "name": "RunloopMcpSpec" - }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopSandboxClient", - "module": "agents.extensions.sandbox", - "name": "RunloopSandboxClient" - }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopSandboxClientOptions", - "module": "agents.extensions.sandbox", - "name": "RunloopSandboxClientOptions" - }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopTunnelConfig", - "module": "agents.extensions.sandbox", - "name": "RunloopTunnelConfig" - }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopUserParameters", - "module": "agents.extensions.sandbox", - "name": "RunloopUserParameters" - }, { "canonical_module": "agents.extensions.sandbox.vercel", "canonical_name": "VercelSandboxClient", @@ -47175,24 +44687,6 @@ "stateful_request" ] }, - { - "class_name": "RunloopPlatformClient", - "module": "agents.extensions.sandbox", - "names": [ - "axons", - "benchmarks", - "blueprints", - "network_policies", - "secrets" - ] - }, - { - "class_name": "RunloopSandboxClient", - "module": "agents.extensions.sandbox", - "names": [ - "platform" - ] - }, { "class_name": "SandboxSessionState", "module": "agents.sandbox.session.sandbox_session_state", @@ -47741,23 +45235,7 @@ "VercelSandboxClient", "VercelSandboxClientOptions", "VercelSandboxSession", - "VercelSandboxSessionState", - "DEFAULT_RUNLOOP_WORKSPACE_ROOT", - "DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT", - "RunloopAfterIdle", - "RunloopExistingSecret", - "RunloopGatewaySpec", - "RunloopLaunchParameters", - "RunloopMcpSpec", - "RunloopPlatformClient", - "RunloopCloudBucketMountStrategy", - "RunloopSandboxClient", - "RunloopSandboxClientOptions", - "RunloopSandboxSession", - "RunloopSandboxSessionState", - "RunloopTimeouts", - "RunloopTunnelConfig", - "RunloopUserParameters" + "VercelSandboxSessionState" ], "optional_bindings": {}, "optional_exports": { @@ -47767,27 +45245,11 @@ "CloudflareSandboxClientOptions": "aiohttp", "CloudflareSandboxSession": "aiohttp", "CloudflareSandboxSessionState": "aiohttp", - "DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT": "runloop_api_client", - "DEFAULT_RUNLOOP_WORKSPACE_ROOT": "runloop_api_client", "ModalCloudBucketMountStrategy": "modal", "ModalSandboxClient": "modal", "ModalSandboxClientOptions": "modal", "ModalSandboxSession": "modal", "ModalSandboxSessionState": "modal", - "RunloopAfterIdle": "runloop_api_client", - "RunloopCloudBucketMountStrategy": "runloop_api_client", - "RunloopExistingSecret": "runloop_api_client", - "RunloopGatewaySpec": "runloop_api_client", - "RunloopLaunchParameters": "runloop_api_client", - "RunloopMcpSpec": "runloop_api_client", - "RunloopPlatformClient": "runloop_api_client", - "RunloopSandboxClient": "runloop_api_client", - "RunloopSandboxClientOptions": "runloop_api_client", - "RunloopSandboxSession": "runloop_api_client", - "RunloopSandboxSessionState": "runloop_api_client", - "RunloopTimeouts": "runloop_api_client", - "RunloopTunnelConfig": "runloop_api_client", - "RunloopUserParameters": "runloop_api_client", "VercelCloudBucketMountStrategy": "vercel", "VercelSandboxClient": "vercel", "VercelSandboxClientOptions": "vercel", diff --git a/tests/fixtures/released_api_contract_policy.json b/tests/fixtures/released_api_contract_policy.json index 471e6ca3f2..c47d5ae574 100644 --- a/tests/fixtures/released_api_contract_policy.json +++ b/tests/fixtures/released_api_contract_policy.json @@ -138,54 +138,6 @@ "module": "agents.extensions.sandbox", "name": "ModalSandboxClientOptions" }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopAfterIdle", - "module": "agents.extensions.sandbox", - "name": "RunloopAfterIdle" - }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopGatewaySpec", - "module": "agents.extensions.sandbox", - "name": "RunloopGatewaySpec" - }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopLaunchParameters", - "module": "agents.extensions.sandbox", - "name": "RunloopLaunchParameters" - }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopMcpSpec", - "module": "agents.extensions.sandbox", - "name": "RunloopMcpSpec" - }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopSandboxClient", - "module": "agents.extensions.sandbox", - "name": "RunloopSandboxClient" - }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopSandboxClientOptions", - "module": "agents.extensions.sandbox", - "name": "RunloopSandboxClientOptions" - }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopTunnelConfig", - "module": "agents.extensions.sandbox", - "name": "RunloopTunnelConfig" - }, - { - "canonical_module": "agents.extensions.sandbox.runloop", - "canonical_name": "RunloopUserParameters", - "module": "agents.extensions.sandbox", - "name": "RunloopUserParameters" - }, { "canonical_module": "agents.extensions.sandbox.vercel", "canonical_name": "VercelSandboxClient", @@ -386,9 +338,6 @@ "redis": { "extra": "redis" }, - "runloop_api_client": { - "extra": "runloop" - }, "sqlalchemy": { "extra": "sqlalchemy" }, @@ -422,27 +371,11 @@ "CloudflareSandboxClientOptions": "aiohttp", "CloudflareSandboxSession": "aiohttp", "CloudflareSandboxSessionState": "aiohttp", - "DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT": "runloop_api_client", - "DEFAULT_RUNLOOP_WORKSPACE_ROOT": "runloop_api_client", "ModalCloudBucketMountStrategy": "modal", "ModalSandboxClient": "modal", "ModalSandboxClientOptions": "modal", "ModalSandboxSession": "modal", "ModalSandboxSessionState": "modal", - "RunloopAfterIdle": "runloop_api_client", - "RunloopCloudBucketMountStrategy": "runloop_api_client", - "RunloopExistingSecret": "runloop_api_client", - "RunloopGatewaySpec": "runloop_api_client", - "RunloopLaunchParameters": "runloop_api_client", - "RunloopMcpSpec": "runloop_api_client", - "RunloopPlatformClient": "runloop_api_client", - "RunloopSandboxClient": "runloop_api_client", - "RunloopSandboxClientOptions": "runloop_api_client", - "RunloopSandboxSession": "runloop_api_client", - "RunloopSandboxSessionState": "runloop_api_client", - "RunloopTimeouts": "runloop_api_client", - "RunloopTunnelConfig": "runloop_api_client", - "RunloopUserParameters": "runloop_api_client", "VercelCloudBucketMountStrategy": "vercel", "VercelSandboxClient": "vercel", "VercelSandboxClientOptions": "vercel", @@ -742,24 +675,6 @@ "agent_registration" ] }, - { - "class_name": "RunloopPlatformClient", - "module": "agents.extensions.sandbox", - "names": [ - "axons", - "benchmarks", - "blueprints", - "network_policies", - "secrets" - ] - }, - { - "class_name": "RunloopSandboxClient", - "module": "agents.extensions.sandbox", - "names": [ - "platform" - ] - }, { "class_name": "SandboxSessionState", "module": "agents.sandbox.session.sandbox_session_state", diff --git a/tests/memory/test_session_helpers.py b/tests/memory/test_session_helpers.py new file mode 100644 index 0000000000..517a67d53e --- /dev/null +++ b/tests/memory/test_session_helpers.py @@ -0,0 +1,117 @@ +import asyncio + +import pytest +from pydantic import ValidationError + +from agents import Agent, RunConfig, Runner, SessionSettings +from agents.memory import await_session_mutation, coerce_session_settings, resolve_session_limit +from agents.testing import ScriptedModel +from examples.memory.custom_session import CustomSession, HistoryStore +from tests.test_responses import get_text_message + + +def test_settings_normalization_and_limit_precedence() -> None: + settings = coerce_session_settings({"limit": 2}) + assert coerce_session_settings(settings) is settings + assert resolve_session_limit(None, settings) == 2 + assert resolve_session_limit(0, settings) == 0 + assert resolve_session_limit(-1, settings) == -1 + assert resolve_session_limit(3, settings) == 3 + assert resolve_session_limit(None, {"limit": 4}) == 4 + assert resolve_session_limit(None, None) is None + assert resolve_session_limit(None, SessionSettings()) is None + with pytest.raises(TypeError, match="Unknown session settings: unknown"): + coerce_session_settings({"unknown": 1}) + with pytest.raises(ValidationError): + coerce_session_settings({"limit": "not-a-number"}) + + +@pytest.mark.asyncio +async def test_mutation_result_and_failure() -> None: + async def succeed() -> int: + return 42 + + async def fail() -> None: + raise ValueError("write failed") + + assert await await_session_mutation(succeed()) == 42 + with pytest.raises(ValueError, match="write failed"): + await await_session_mutation(fail()) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("fail", [False, True]) +async def test_repeated_cancellation_waits_for_mutation(fail: bool) -> None: + started = asyncio.Event() + finish = asyncio.Event() + settled = asyncio.Event() + writes: list[str] = [] + cancellations: list[str] = [] + + async def write() -> None: + started.set() + try: + await finish.wait() + if fail: + raise ValueError("write failed") + writes.append("committed") + finally: + settled.set() + + async def mutate() -> None: + try: + await await_session_mutation(write()) + except asyncio.CancelledError as exc: + # Python 3.10 can discard this message when another task awaits us. + cancellations.append(str(exc)) + raise + + task = asyncio.create_task(mutate()) + try: + await started.wait() + task.cancel("first") + await asyncio.sleep(0) + task.cancel("second") + await asyncio.sleep(0) + assert not task.done() + assert not settled.is_set() + finish.set() + with pytest.raises(asyncio.CancelledError): + await task + assert cancellations == ["first"] + assert settled.is_set() + assert writes == ([] if fail else ["committed"]) + finally: + finish.set() + await asyncio.gather(task, return_exceptions=True) + + +@pytest.mark.asyncio +async def test_custom_adapter_runner_and_storage_contract() -> None: + store = HistoryStore() + session = CustomSession("first", store, session_settings={"limit": 2}) + other = CustomSession("other", store) + model = ScriptedModel([[get_text_message("Hello!")], [get_text_message("You said hello.")]]) + agent = Agent(name="Assistant", model=model) + config = RunConfig(tracing_disabled=True) + await Runner.run(agent, "Hello", session=session, run_config=config) + await Runner.run(agent, "What did I say?", session=session, run_config=config) + assert model.calls[-1].input[0]["content"] == "Hello" + assert len(await session.get_items()) == 2 + assert len(await session.get_items(limit=4)) == 4 + assert await session.get_items(limit=0) == [] + assert await session.get_items(limit=-1) == [] + assert await other.get_items() == [] + last = (await session.get_items())[-1] + assert await session.pop_item() == last + await session.clear_session() + assert await session.pop_item() is None + assert await session.get_items() == [] + + +@pytest.mark.asyncio +async def test_independently_cancelled_operation_propagates() -> None: + operation: asyncio.Future[int] = asyncio.get_running_loop().create_future() + operation.cancel() + with pytest.raises(asyncio.CancelledError): + await await_session_mutation(operation) diff --git a/tests/models/test_default_models.py b/tests/models/test_default_models.py index 9fb1f37d5c..1b72afa4dc 100644 --- a/tests/models/test_default_models.py +++ b/tests/models/test_default_models.py @@ -199,3 +199,50 @@ def test_agent_resets_model_settings_for_non_gpt_5_models(): agent = Agent(name="test", model="gpt-4.1") assert agent.model == "gpt-4.1" assert agent.model_settings == ModelSettings() + + +@pytest.mark.parametrize("model_name", [None, "gpt-4o", "gpt-5"]) +def test_explicit_default_settings_survive_construction_and_clone( + monkeypatch: pytest.MonkeyPatch, model_name: str | None +) -> None: + monkeypatch.setenv("OPENAI_DEFAULT_MODEL", "gpt-5.6-luna") + settings = get_default_model_settings() + agent = Agent(name="test", model=model_name, model_settings=settings) + cloned = agent.clone(model="gpt-4.1") + assert agent.model_settings is settings + assert cloned.model_settings is settings + + +def test_clone_retains_implicit_defaults_without_copying_public_settings() -> None: + agent = Agent(name="test", model="gpt-5") + cloned = agent.clone(name="clone") + assert cloned.model_settings is agent.model_settings + assert cloned.clone(model="gpt-4.1").model_settings == ModelSettings() + + +def test_clone_explicit_settings_equal_to_defaults_are_not_recomputed() -> None: + agent = Agent(name="test", model="gpt-5") + explicit = get_default_model_settings("gpt-5") + cloned = agent.clone(model_settings=explicit).clone(model="gpt-4.1") + assert cloned.model_settings is explicit + + +@pytest.mark.parametrize("replacement", [False, True]) +def test_clone_preserves_settings_customized_after_construction(replacement: bool) -> None: + agent = Agent(name="test", model="gpt-5") + if replacement: + # Assigning an equal object is still an explicit settings choice. + agent.model_settings = get_default_model_settings("gpt-5") + else: + agent.model_settings.temperature = 0.3 + settings = agent.model_settings + assert agent.clone(model="gpt-4.1").model_settings is settings + + +def test_explicit_empty_dictionary_settings_are_preserved( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("OPENAI_DEFAULT_MODEL", "gpt-4o") + agent = Agent(name="test", model="gpt-5", model_settings={}) + assert agent.model_settings == ModelSettings() + assert agent.clone(model="gpt-5.6-luna").model_settings == ModelSettings() diff --git a/tests/models/test_gpt6_chat_tool_defaults.py b/tests/models/test_gpt6_chat_tool_defaults.py new file mode 100644 index 0000000000..f01793f164 --- /dev/null +++ b/tests/models/test_gpt6_chat_tool_defaults.py @@ -0,0 +1,178 @@ +"""Inspect real Runner request serialization using a framework HTTP mock.""" + +import json +from typing import Any + +import httpx2 +import pytest +from openai import AsyncOpenAI +from openai.types.shared import Reasoning + +from agents import Agent, ModelSettings, OpenAIChatCompletionsModel, RunConfig, Runner +from agents.decorators import tool +from agents.models.openai_provider import OpenAIProvider + + +@tool +def weather(city: str) -> str: + """Get weather for a city.""" + return "sunny" + + +@pytest.mark.allow_call_model_methods +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("name", "entry", "tools", "settings", "responses", "expected"), + [ + ("gpt-6-sol", "string", "function", ModelSettings(), False, "none"), + ("gpt-6-luna", "direct", "function", ModelSettings(), False, "none"), + ("gpt-6-sol", "override", "handoff", ModelSettings(), False, "none"), + ( + "gpt-6-sol", + "direct", + "function", + ModelSettings(reasoning=Reasoning(effort="high")), + False, + "high", + ), + ( + "gpt-6-sol", + "direct", + "function", + ModelSettings(extra_args={"reasoning_effort": "low"}), + False, + "low", + ), + ( + "gpt-6-sol", + "direct", + "function", + ModelSettings(extra_body={"reasoning_effort": "low"}), + False, + "low", + ), + ("gpt-6-sol", "string", "none", ModelSettings(), False, None), + ("gpt-6.1-sol", "string", "function", ModelSettings(), False, None), + ("gpt-6-astra", "string", "function", ModelSettings(), False, None), + ("gpt-6-sol-unknown", "string", "function", ModelSettings(), False, None), + ("gpt-6-sol", "string", "function", ModelSettings(), True, None), + ], +) +@pytest.mark.parametrize("stream", [False, True]) +async def test_gpt6_tool_request_defaults( + name: str, + entry: str, + tools: str, + settings: ModelSettings, + responses: bool, + expected: str | None, + stream: bool, +) -> None: + if responses and stream: + pytest.skip("Responses preservation is covered by the non-streaming request.") + before = settings.to_json_dict() + bodies: list[dict[str, Any]] = [] + + def handler(request: httpx2.Request) -> httpx2.Response: + assert request.url.path == ("/v1/responses" if responses else "/v1/chat/completions") + bodies.append(json.loads(request.content)) + if responses: + return httpx2.Response( + 200, + json={ + "id": "resp_test", + "object": "response", + "created_at": 0, + "model": name, + "status": "completed", + "output": [], + "parallel_tool_calls": False, + "tool_choice": "auto", + "tools": [], + }, + ) + if stream: + chunks = [ + { + "id": "chatcmpl_test", + "object": "chat.completion.chunk", + "created": 0, + "model": name, + "choices": [ + { + "index": 0, + "delta": {"role": "assistant", "content": "sunny"}, + "finish_reason": None, + } + ], + }, + { + "id": "chatcmpl_test", + "object": "chat.completion.chunk", + "created": 0, + "model": name, + "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}], + }, + ] + return httpx2.Response( + 200, + headers={"content-type": "text/event-stream"}, + text="".join(f"data: {json.dumps(chunk)}\n\n" for chunk in chunks) + + "data: [DONE]\n\n", + ) + return httpx2.Response( + 200, + json={ + "id": "chatcmpl_test", + "object": "chat.completion", + "created": 0, + "model": name, + "choices": [ + { + "index": 0, + "finish_reason": "stop", + "message": {"role": "assistant", "content": "sunny"}, + } + ], + }, + ) + + async with AsyncOpenAI( + api_key="test-key", + base_url="https://example.invalid/v1", + http_client=httpx2.AsyncClient(transport=httpx2.MockTransport(handler)), + ) as client: + provider = OpenAIProvider(openai_client=client, use_responses=responses) + model = ( + OpenAIChatCompletionsModel(model=name, openai_client=client) + if entry == "direct" + else name + ) + agent = Agent( + name="test", + model=model, + model_settings=settings, + tools=[weather] if tools == "function" else [], + handoffs=[Agent(name="other", model=name)] if tools == "handoff" else [], + ) + config = RunConfig( + model_provider=provider, + model=name if entry == "override" else None, + tracing_disabled=True, + ) + if stream: + result = Runner.run_streamed(agent, "Weather?", run_config=config) + async for _event in result.stream_events(): + pass + else: + await Runner.run(agent, "Weather?", run_config=config) + assert len(bodies) == 1 + body = bodies[0] + if responses: + assert "reasoning" not in body + elif expected is None: + assert "reasoning_effort" not in body + else: + assert body["reasoning_effort"] == expected + assert bool(body.get("tools")) == (tools != "none") + assert settings.to_json_dict() == before diff --git a/tests/models/test_openai_chatcompletions.py b/tests/models/test_openai_chatcompletions.py index 1f6710e889..461aedbbe9 100644 --- a/tests/models/test_openai_chatcompletions.py +++ b/tests/models/test_openai_chatcompletions.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json import logging from collections.abc import AsyncIterator from typing import Any, cast @@ -43,7 +44,9 @@ ModelSettings, ModelTracing, OpenAIChatCompletionsModel, + OpenAIConversationsSession, OpenAIProvider, + RunConfig, Runner, __version__, function_tool, @@ -74,6 +77,99 @@ def _minimal_chat_completion(content: str = "ok") -> ChatCompletion: ) +@pytest.mark.allow_call_model_methods +@pytest.mark.asyncio +@pytest.mark.parametrize("stream", [False, True], ids=["non-streaming", "streaming"]) +@pytest.mark.parametrize("include_status", [True, False], ids=["with-status", "statusless"]) +async def test_runner_replays_openai_conversation_assistant_history( + stream: bool, include_status: bool +) -> None: + # Mock the transport so the real session sanitizer and provider converter both run. + history = [ + { + "id": "msg_user", + "type": "message", + "role": "user", + "status": "completed", + "content": [{"type": "input_text", "text": "My name is Ada."}], + }, + { + "id": "msg_assistant", + "type": "message", + "role": "assistant", + "status": "completed", + "content": [{"type": "output_text", "text": "Hello, Ada!", "annotations": []}], + }, + ] + if not include_status: + history[1].pop("status") + requests: list[dict[str, Any]] = [] + + def handler(request: httpx2.Request) -> httpx2.Response: + if request.url.path == "/v1/conversations/conv_test/items": + assert request.method in {"GET", "POST"} + return httpx2.Response( + 200, + json={ + "object": "list", + "data": history if request.method == "GET" else [], + "has_more": False, + }, + ) + assert request.method == "POST" + assert request.url.path == "/v1/chat/completions" + requests.append(json.loads(request.content)) + if stream: + chunk = { + "id": "chatcmpl-test", + "object": "chat.completion.chunk", + "created": 0, + "model": "fake", + "choices": [ + { + "index": 0, + "finish_reason": "stop", + "delta": {"role": "assistant", "content": "Your name is Ada."}, + } + ], + } + return httpx2.Response( + 200, + headers={"content-type": "text/event-stream"}, + content=f"data: {json.dumps(chunk)}\n\ndata: [DONE]\n\n", + ) + return httpx2.Response(200, json=_minimal_chat_completion("Your name is Ada.").model_dump()) + + async with AsyncOpenAI( + api_key="test-placeholder", + base_url="https://provider.example/v1", + max_retries=0, + http_client=httpx2.AsyncClient(transport=httpx2.MockTransport(handler)), + ) as client: + session = OpenAIConversationsSession(conversation_id="conv_test", openai_client=client) + agent = Agent( + name="test", model=OpenAIChatCompletionsModel(model="fake", openai_client=client) + ) + config = RunConfig(tracing_disabled=True) + if stream: + result = Runner.run_streamed( + agent, "What is my name?", session=session, run_config=config + ) + async for _ in result.stream_events(): + pass + else: + result = await Runner.run(agent, "What is my name?", session=session, run_config=config) + + assert result.final_output == "Your name is Ada." + assert len(requests) == 1 + assert requests[0]["messages"] == [ + {"role": "user", "content": [{"type": "text", "text": "My name is Ada."}]}, + {"role": "assistant", "content": "Hello, Ada!"}, + {"role": "user", "content": "What is my name?"}, + ] + assert history[1]["id"] == "msg_assistant" + + async def _run_chat_completions_model_with_custom_base_url( model_settings: ModelSettings | dict[str, Any] | None = None, tools: list[Tool] | None = None, diff --git a/tests/models/test_openai_chatcompletions_converter.py b/tests/models/test_openai_chatcompletions_converter.py index 22d6779a3a..5b97e4fb5c 100644 --- a/tests/models/test_openai_chatcompletions_converter.py +++ b/tests/models/test_openai_chatcompletions_converter.py @@ -321,7 +321,8 @@ def test_items_to_messages_accepts_raw_chat_completions_user_content_parts(): ] -def test_items_to_messages_with_output_message_and_function_call(): +@pytest.mark.parametrize("include_id", [True, False], ids=["with-id", "session-replay"]) +def test_items_to_messages_with_output_message_and_function_call(include_id: bool): """ Given a sequence of one ResponseOutputMessageParam followed by a ResponseFunctionToolCallParam, the converter should produce a single @@ -354,8 +355,11 @@ def test_items_to_messages_with_output_message_and_function_call(): "arguments": "{}", "type": "function_call", } + message = resp_msg.model_dump() + if not include_id: + message.pop("id") items: list[TResponseInputItem] = [ - resp_msg.model_dump(), # type:ignore + cast(TResponseInputItem, message), func_item, ] messages = Converter.items_to_messages(items) @@ -379,6 +383,28 @@ def test_items_to_messages_with_output_message_and_function_call(): assert tool_call["function"]["arguments"] == "{}" +@pytest.mark.parametrize( + "content", + [ + "hello", + [{"type": "input_text", "text": "hello"}], + [{"type": "output_audio", "data": "AAAA"}], + ], +) +def test_idless_assistant_output_rejects_unsupported_content(content: Any): + item = cast( + TResponseInputItem, + { + "type": "message", + "role": "assistant", + "status": "completed", + "content": content, + }, + ) + with pytest.raises(UserError, match="Unhandled item type or structure"): + Converter.items_to_messages([item]) + + def _turn_items_function_call_first() -> list[TResponseInputItem]: """One completed tool turn in the order the streaming handler emits it. @@ -492,7 +518,8 @@ def test_items_to_messages_merges_refusal_into_pending_tool_call_message(): assert assistant.get("tool_calls") -def test_items_to_messages_accepts_statusless_output_message(): +@pytest.mark.parametrize("include_id", [True, False], ids=["with-id", "session-replay"]) +def test_items_to_messages_accepts_statusless_output_message(include_id: bool): """Output messages remain recognizable after replay normalization removes null status.""" statusless_message = cast( TResponseInputItem, @@ -509,6 +536,8 @@ def test_items_to_messages_accepts_statusless_output_message(): ], }, ) + if not include_id: + statusless_message.pop("id") assert Converter.items_to_messages([statusless_message]) == [ {"role": "assistant", "content": "hello"} diff --git a/tests/models/test_responses_stream_diagnostics.py b/tests/models/test_responses_stream_diagnostics.py new file mode 100644 index 0000000000..2b557cab54 --- /dev/null +++ b/tests/models/test_responses_stream_diagnostics.py @@ -0,0 +1,132 @@ +"""Exercise missing-completion diagnostics through the real SSE parser and runner.""" + +import json +from typing import Any + +import httpx2 +import pytest +from openai import AsyncOpenAI + +from agents import Agent, ModelSettings, RunConfig, Runner +from agents.exceptions import ModelBehaviorError +from agents.models.openai_responses import OpenAIResponsesModel +from agents.retry import ModelRetrySettings +from tests.model_test_helpers import get_response_obj +from tests.test_responses import get_text_message + +pytestmark = [pytest.mark.asyncio, pytest.mark.allow_call_model_methods] + + +def _events(terminal: str | None = None) -> list[dict[str, Any]]: + response = get_response_obj([], response_id="resp_synthetic").model_dump() + response["status"] = "in_progress" + response["instructions"] = "private synthetic instructions" + events: list[dict[str, Any]] = [ + {"type": "response.created", "sequence_number": 0, "response": response}, + { + "type": "response.output_text.delta", + "sequence_number": 1, + "item_id": "msg_synthetic", + "output_index": 0, + "content_index": 0, + "delta": "private synthetic output", + "logprobs": [], + }, + ] + if terminal is not None: + final_response = get_response_obj( + [get_text_message("Answer")], response_id="resp_synthetic" + ).model_dump() + final_response["status"] = terminal + if terminal == "incomplete": + final_response["incomplete_details"] = {"reason": "max_output_tokens"} + events.append( + { + "type": f"response.{terminal}", + "sequence_number": 2, + "response": final_response, + } + ) + return events + + +def _body(events: list[dict[str, Any]]) -> bytes: + # Mock the HTTP transport, retaining real event parsing and request-ID propagation. + return "".join( + f"event: {event['type']}\ndata: {json.dumps(event)}\n\n" for event in events + ).encode() + + +@pytest.mark.parametrize("empty", [False, True], ids=["partial-output", "empty"]) +@pytest.mark.parametrize("request_id", ["req_synthetic", None], ids=["request-id", "no-request-id"]) +async def test_missing_completion_reports_observed_metadata( + empty: bool, request_id: str | None +) -> None: + headers = {"content-type": "text/event-stream"} + if request_id is not None: + headers["x-request-id"] = request_id + http_response = httpx2.Response(200, headers=headers, content=_body([] if empty else _events())) + requests: list[httpx2.Request] = [] + + def respond(request: httpx2.Request) -> httpx2.Response: + requests.append(request) + return http_response + + async with AsyncOpenAI( + api_key="synthetic", + http_client=httpx2.AsyncClient(transport=httpx2.MockTransport(respond), trust_env=False), + ) as client: + result = Runner.run_streamed( + Agent( + name="Synthetic", + model=OpenAIResponsesModel("synthetic-model", client), + model_settings=ModelSettings( + retry=ModelRetrySettings(max_retries=1, policy=lambda _: True) + ), + ), + "private synthetic input", + run_config=RunConfig(trace_include_sensitive_data=False), + ) + with pytest.raises(ModelBehaviorError) as exc_info: + async for _ in result.stream_events(): + pass + + message = str(exc_info.value) + assert message.startswith("Model did not produce a final response!") + assert "stream ended before a response.completed event was received" in message + assert f"event_count={0 if empty else 2}" in message + assert f"last_event_type={None if empty else 'response.output_text.delta'!r}" in message + assert f"response_id={None if empty else 'resp_synthetic'!r}" in message + assert f"request_id={None if empty else request_id!r}" in message + assert "private synthetic" not in message + assert len(requests) == 1 + assert result.final_output is None + assert http_response.is_closed + + +@pytest.mark.parametrize("terminal", ["completed", "incomplete", "failed"]) +async def test_explicit_terminal_retains_existing_behavior(terminal: str) -> None: + http_response = httpx2.Response( + 200, content=_body(_events(terminal)), headers={"content-type": "text/event-stream"} + ) + async with AsyncOpenAI( + api_key="synthetic", + http_client=httpx2.AsyncClient( + transport=httpx2.MockTransport(lambda _: http_response), trust_env=False + ), + ) as client: + result = Runner.run_streamed( + Agent(name="Synthetic", model=OpenAIResponsesModel("synthetic-model", client)), + "Answer", + run_config=RunConfig(tracing_disabled=True), + ) + if terminal == "completed": + async for _ in result.stream_events(): + pass + assert result.final_output == "Answer" + else: + with pytest.raises(ModelBehaviorError, match=f"terminal event `response.{terminal}`"): + async for _ in result.stream_events(): + pass + assert result.final_output is None + assert http_response.is_closed diff --git a/tests/realtime/test_openai_realtime.py b/tests/realtime/test_openai_realtime.py index d5d18fc7bc..912fbe7ae4 100644 --- a/tests/realtime/test_openai_realtime.py +++ b/tests/realtime/test_openai_realtime.py @@ -1478,6 +1478,47 @@ async def test_send_event_dispatch(self, model, monkeypatch): # session update -> 1 assert send_raw.await_count == 8 + @pytest.mark.asyncio + @pytest.mark.parametrize("automatic_interrupt", [True, False]) + @pytest.mark.parametrize("response_active", [True, False]) + async def test_session_interrupt_cancels_active_text_response( + self, model, monkeypatch, automatic_interrupt, response_active + ): + model._created_session = RealtimeSessionCreateRequest.model_validate( + { + "type": "realtime", + "model": "gpt-realtime-2.1", + "output_modalities": ["text"], + "audio": { + "input": { + "turn_detection": { + "type": "semantic_vad", + "interrupt_response": automatic_interrupt, + } + } + }, + } + ) + await model._mark_response_created("response_1") + if not response_active: + await model._mark_response_done("response_1") + send_raw = AsyncMock() + monkeypatch.setattr(model, "_send_raw_message", send_raw) + session = RealtimeSession(model, RealtimeAgent(name="agent"), None) + + # A text/button interruption supplies no speech event to trigger VAD cancellation. + await session.interrupt() + await session.interrupt() + + if response_active: + send_raw.assert_awaited_once() + assert send_raw.await_args is not None + assert send_raw.await_args.args[0].model_dump(exclude_unset=True) == { + "type": "response.cancel" + } + else: + send_raw.assert_not_awaited() + @pytest.mark.asyncio async def test_interrupt_force_cancel_overrides_auto_cancellation(self, model, monkeypatch): """Interrupt should send response.cancel even when auto cancel is enabled.""" diff --git a/tests/realtime/test_session.py b/tests/realtime/test_session.py index be2366e0f1..db3cd3bf65 100644 --- a/tests/realtime/test_session.py +++ b/tests/realtime/test_session.py @@ -185,7 +185,10 @@ async def test_property_and_send_helpers_and_enter_alias(): # verify sent events assert any(isinstance(e, RealtimeModelSendUserInput) for e in model.events) assert any(isinstance(e, RealtimeModelSendAudio) and e.commit for e in model.events) - assert any(isinstance(e, RealtimeModelSendInterrupt) for e in model.events) + assert any( + isinstance(e, RealtimeModelSendInterrupt) and e.force_response_cancel + for e in model.events + ) finally: await session.close() diff --git a/tests/sandbox/test_bounded_read.py b/tests/sandbox/test_bounded_read.py index 7fe6d7e124..aaf39cfeae 100644 --- a/tests/sandbox/test_bounded_read.py +++ b/tests/sandbox/test_bounded_read.py @@ -70,10 +70,9 @@ def close(self) -> None: self.closed = True -PROVIDERS = ["e2b", "runloop", "vercel", "daytona", "blaxel", "cloudflare"] +PROVIDERS = ["e2b", "vercel", "daytona", "blaxel", "cloudflare"] CLASSES = { "e2b": "E2BSandboxSession", - "runloop": "RunloopSandboxSession", "vercel": "VercelSandboxSession", "daytona": "DaytonaSandboxSession", "blaxel": "BlaxelSandboxSession", @@ -94,19 +93,6 @@ def _session(provider: str, content: _Content, *, status: int = 200) -> Any: session._sandbox = SimpleNamespace( files=SimpleNamespace(read=AsyncMock(return_value=content)) ) - elif provider == "runloop": - session.state = SimpleNamespace( - devbox_id="test", timeouts=SimpleNamespace(file_download_s=30) - ) - session._sdk = SimpleNamespace( - api=SimpleNamespace( - devboxes=SimpleNamespace( - with_streaming_response=SimpleNamespace( - download_file=MagicMock(return_value=content) - ) - ) - ) - ) elif provider == "vercel": @asynccontextmanager @@ -346,7 +332,7 @@ async def chunks(self, chunk_size: int = 3) -> AsyncIterator[bytes]: "provider,retryable", [ (provider, retryable) - for provider in ["cloudflare", "vercel", "runloop", "modal", "daytona", "e2b"] + for provider in ["cloudflare", "vercel", "modal", "daytona", "e2b"] for retryable in [True, False] ] + [("blaxel", True), ("blaxel", None)], @@ -405,26 +391,6 @@ async def test_bounded_provider_read_preserves_retry_policy( inner._ensure_sandbox.return_value.iter_file.side_effect = error_cls( httpx.Response(429 if retryable else 403), "synthetic-private-response" ) - elif provider == "runloop": - import runloop_api_client - - from agents.extensions.sandbox.runloop.sandbox import RunloopSandboxSessionState - - inner.state = RunloopSandboxSessionState( - devbox_id="test", manifest=Manifest(), snapshot=NoopSnapshot(id="test") - ) - error_cls = ( - runloop_api_client.RateLimitError - if retryable - else runloop_api_client.PermissionDeniedError - ) - inner._sdk.api.devboxes.with_streaming_response.download_file.side_effect = error_cls( - "synthetic-private-response", - response=httpx.Response( - 429 if retryable else 403, request=httpx.Request("GET", "https://example.invalid") - ), - body={"detail": "synthetic-private-response"}, - ) events: list[SandboxSessionEvent] = [] wrapper = SandboxSession( inner, @@ -517,40 +483,6 @@ async def readexactly(self, size: int) -> bytes: assert not session.mount_active -@pytest.mark.asyncio -async def test_runloop_bounded_read_uses_streaming_sdk_response() -> None: - import httpx - - pytest.importorskip("runloop_api_client") - from runloop_api_client import AsyncRunloop - - consumed = 0 - closed = False - - class Body(httpx.AsyncByteStream): - async def __aiter__(self) -> AsyncIterator[bytes]: - nonlocal consumed - for _ in range(10): - consumed += 1 - yield b"x" * 65536 - - async def aclose(self) -> None: - nonlocal closed - closed = True - - def respond(request: httpx.Request) -> httpx.Response: - assert request.url.path.endswith("/download_file") - return httpx.Response(200, stream=Body()) - - async with httpx.AsyncClient(transport=httpx.MockTransport(respond)) as http: - async with AsyncRunloop(bearer_token="synthetic-test-key", http_client=http) as api: - session = _session("runloop", _Content(b"unused")) - session._sdk = SimpleNamespace(api=api) - assert await session.read_bounded(Path("out.jsonl"), max_bytes=5) == b"xxxxx" - assert consumed == 1 - assert closed - - @pytest.mark.asyncio async def test_e2b_bounded_read_closes_real_sdk_stream() -> None: import httpx diff --git a/tests/sandbox/test_compatibility_guards.py b/tests/sandbox/test_compatibility_guards.py index b1ff30771f..67df891adb 100644 --- a/tests/sandbox/test_compatibility_guards.py +++ b/tests/sandbox/test_compatibility_guards.py @@ -299,34 +299,6 @@ def test_core_sandbox_public_export_surface_is_stable() -> None: "CloudflareSandboxSessionState", }, ), - ( - "agents.extensions.sandbox.runloop", - { - "DEFAULT_RUNLOOP_WORKSPACE_ROOT", - "DEFAULT_RUNLOOP_ROOT_WORKSPACE_ROOT", - "RunloopAfterIdle", - "RunloopExistingSecret", - "RunloopGatewaySpec", - "RunloopLaunchParameters", - "RunloopMcpSpec", - "RunloopPlatformAxonsClient", - "RunloopPlatformBenchmarksClient", - "RunloopPlatformBlueprintsClient", - "RunloopPlatformClient", - "RunloopPlatformNetworkPoliciesClient", - "RunloopPlatformSecretsClient", - "RunloopCloudBucketMountStrategy", - "RunloopSandboxClient", - "RunloopSandboxClientOptions", - "RunloopSandboxSession", - "RunloopSandboxSessionState", - "RunloopTimeouts", - "RunloopTunnelConfig", - "RunloopUserParameters", - "_decode_runloop_snapshot_ref", - "_encode_runloop_snapshot_ref", - }, - ), ( "agents.extensions.sandbox.vercel", { @@ -480,26 +452,6 @@ def test_optional_sandbox_dataclass_constructor_field_order_is_stable( "exposed_port_url_ttl_s", ), ), - ( - "agents.extensions.sandbox.runloop", - "RunloopSandboxClientOptions", - ( - "blueprint_id", - "blueprint_name", - "env_vars", - "pause_on_exit", - "name", - "timeouts", - "exposed_ports", - "user_parameters", - "launch_parameters", - "tunnel", - "gateways", - "mcp", - "metadata", - "managed_secrets", - ), - ), ( "agents.extensions.sandbox.vercel", "VercelSandboxClientOptions", @@ -704,34 +656,6 @@ def test_optional_sandbox_client_options_positional_field_order_is_stable( "exposed_port_url_ttl_s", ), ), - ( - "agents.extensions.sandbox.runloop", - "RunloopSandboxSessionState", - ( - "type", - "session_id", - "snapshot", - "manifest", - "exposed_ports", - "snapshot_fingerprint", - "snapshot_fingerprint_version", - "workspace_root_ready", - "devbox_id", - "blueprint_id", - "blueprint_name", - "base_env_vars", - "pause_on_exit", - "name", - "timeouts", - "user_parameters", - "launch_parameters", - "tunnel", - "gateways", - "mcp", - "metadata", - "secret_refs", - ), - ), ( "agents.extensions.sandbox.vercel", "VercelSandboxSessionState", @@ -798,7 +722,6 @@ def test_sandbox_session_state_field_order_is_stable( "cloudflare", ), ("agents.extensions.sandbox.daytona", "DaytonaSandboxClientOptions", (), "daytona"), - ("agents.extensions.sandbox.runloop", "RunloopSandboxClientOptions", (), "runloop"), ("agents.extensions.sandbox.vercel", "VercelSandboxClientOptions", (), "vercel"), ], ) @@ -855,11 +778,6 @@ def test_optional_sandbox_client_options_json_round_trip_preserves_type( "BlaxelSandboxSessionState", {"sandbox_name": "sandbox-123"}, ), - ( - "agents.extensions.sandbox.runloop", - "RunloopSandboxSessionState", - {"devbox_id": "devbox-123"}, - ), ( "agents.extensions.sandbox.vercel", "VercelSandboxSessionState", @@ -969,11 +887,6 @@ def test_mount_strategy_type_strings_round_trip_through_registry( "blaxel_cloud_bucket", ), ("agents.extensions.sandbox.blaxel", "BlaxelDriveMountStrategy", "blaxel_drive"), - ( - "agents.extensions.sandbox.runloop", - "RunloopCloudBucketMountStrategy", - "runloop_cloud_bucket", - ), ( "agents.extensions.sandbox.vercel", "VercelCloudBucketMountStrategy", diff --git a/tests/sandbox/test_external_provider_example.py b/tests/sandbox/test_external_provider_example.py new file mode 100644 index 0000000000..59e85685f1 --- /dev/null +++ b/tests/sandbox/test_external_provider_example.py @@ -0,0 +1,141 @@ +"""Check the external package example against the SDK without a Docker daemon.""" + +import importlib +from pathlib import Path +from types import ModuleType +from unittest.mock import create_autospec + +import pytest +from pydantic import ValidationError + +from agents import RunConfig, Runner +from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig +from agents.sandbox.sandboxes.docker import ( + DockerSandboxClient, + DockerSandboxClientOptions, + DockerSandboxSessionState, +) +from agents.sandbox.session import BaseSandboxClientOptions, SandboxSession +from agents.sandbox.snapshot import NoopSnapshot +from agents.testing import ScriptedModel, assistant_message + + +@pytest.fixture +def provider(monkeypatch: pytest.MonkeyPatch) -> ModuleType: + source = Path(__file__).resolve().parents[2] / "examples/sandbox/external_provider/src" + monkeypatch.syspath_prepend(str(source)) + return importlib.import_module("example_docker_provider") + + +def test_options_register_on_import(provider: ModuleType) -> None: + options = BaseSandboxClientOptions.parse( + {"type": "example_docker", "image": "python:3.14-slim"} + ) + assert isinstance(options, provider.ExampleDockerOptions) + assert options.model_dump() == {"type": "example_docker", "image": "python:3.14-slim"} + with pytest.raises(ValidationError): + provider.ExampleDockerOptions(image="") + + +@pytest.mark.asyncio +async def test_create_preserves_inputs_and_instrumented_session(provider: ModuleType) -> None: + delegate = create_autospec(DockerSandboxClient, instance=True) + session = create_autospec(SandboxSession, instance=True) + delegate.create.return_value = session + client = provider.ExampleDockerClient(delegate) + manifest = Manifest() + snapshot = NoopSnapshot(id="example-snapshot") + + config = SandboxRunConfig( + client=client, options=provider.ExampleDockerOptions(image="python:3.14-slim") + ) + result = await client.create( + manifest=manifest, + snapshot=snapshot, + options=config.options, + ) + + assert result is session + delegate.create.assert_awaited_once_with( + manifest=manifest, + snapshot=snapshot, + options=DockerSandboxClientOptions(image="python:3.14-slim"), + ) + + +@pytest.mark.asyncio +async def test_resume_and_delete_preserve_delegate_ownership(provider: ModuleType) -> None: + delegate = create_autospec(DockerSandboxClient, instance=True) + session = create_autospec(SandboxSession, instance=True) + delegate.resume.return_value = session + delegate.delete.return_value = session + client = provider.ExampleDockerClient(delegate) + state = DockerSandboxSessionState( + image="python:3.14-slim", + container_id="example-container", + manifest=Manifest(), + snapshot=NoopSnapshot(id="example-snapshot"), + ) + + resumed_session = await client.resume(state) + assert resumed_session is session + deleted_session = await client.delete(session) + assert deleted_session is session + delegate.resume.assert_awaited_once_with(state) + delegate.delete.assert_awaited_once_with(session) + assert client.backend_id == DockerSandboxClient.backend_id == "docker" + + +def test_docker_state_round_trip_uses_real_delegate(provider: ModuleType) -> None: + # Serialization needs no Docker connection; no Docker transport method is invoked. + from docker import DockerClient + + connection = create_autospec(DockerClient, instance=True) + delegate = DockerSandboxClient(connection) + client = provider.ExampleDockerClient(delegate) + state = DockerSandboxSessionState( + image="python:3.14-slim", + container_id="example-container", + manifest=Manifest(), + snapshot=NoopSnapshot(id="example-snapshot"), + ) + + payload = client.serialize_session_state(state) + restored = client.deserialize_session_state(payload) + + assert payload["type"] == "docker" + assert isinstance(restored, DockerSandboxSessionState) + assert restored.container_id == state.container_id + assert restored.session_id == state.session_id + assert restored.image == state.image + + +def test_example_options_reject_unsupported_network_setting(provider: ModuleType) -> None: + with pytest.raises(ValidationError, match="network_mode"): + provider.ExampleDockerOptions(image="python:3.14-slim", network_mode="none") + + +@pytest.mark.asyncio +@pytest.mark.parametrize("options_kind", ["dictionary", "tagged-dictionary", "docker-options"]) +async def test_runner_rejects_docker_options_before_delegation( + provider: ModuleType, options_kind: str +) -> None: + delegate = create_autospec(DockerSandboxClient, instance=True) + client = provider.ExampleDockerClient(delegate) + payload = {"image": "python:3.14-slim", "network_mode": "none"} + if options_kind == "tagged-dictionary": + payload["type"] = "docker" + options = DockerSandboxClientOptions(**payload) if options_kind == "docker-options" else payload + config = SandboxRunConfig(client=client, options=options) + assert isinstance(config.options, DockerSandboxClientOptions) + assert config.options.network_mode == "none" + model = ScriptedModel([[assistant_message("done")]]) + agent = SandboxAgent(name="External provider", model=model) + + with pytest.raises(TypeError, match="requires an ExampleDockerOptions instance"): + await Runner.run( + agent, "hello", run_config=RunConfig(tracing_disabled=True, sandbox=config) + ) + + delegate.create.assert_not_awaited() + assert not model.calls diff --git a/tests/sandbox/test_helper_startup.py b/tests/sandbox/test_helper_startup.py new file mode 100644 index 0000000000..3ea8a1e952 --- /dev/null +++ b/tests/sandbox/test_helper_startup.py @@ -0,0 +1,155 @@ +from __future__ import annotations + +import io +import json +import shlex +import sys +import tarfile +from pathlib import Path +from typing import TYPE_CHECKING, Any + +import pytest +from openai.types.responses import ResponseCustomToolCall + +from agents import RunConfig, Runner +from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig +from agents.sandbox.capabilities import Filesystem, Shell +from agents.sandbox.capabilities.shell import ShellToolSet +from agents.sandbox.session import SandboxSession +from agents.sandbox.session.base_sandbox_session import BaseSandboxSession +from agents.sandbox.snapshot import NoopSnapshotSpec +from agents.testing import ModelStep, ScriptedModel +from tests.test_responses import get_final_output_message, get_function_tool_call + +pytestmark = pytest.mark.skipif(sys.platform == "win32", reason="requires POSIX helpers") + + +if TYPE_CHECKING or sys.platform != "win32": + from agents.sandbox.sandboxes.unix_local import UnixLocalSandboxClient + + class _RecordingClient(UnixLocalSandboxClient): + def __init__(self) -> None: + super().__init__(inherit_host_environment=False) + self.sessions: list[SandboxSession] = [] + + async def create(self, **kwargs: Any) -> SandboxSession: + session = await super().create(**kwargs) + self.sessions.append(session) + return session + + +@pytest.mark.asyncio +@pytest.mark.parametrize("approval", [None, False, True], ids=["file-only", "rejected", "approved"]) +async def test_runner_snapshot_does_not_execute_workspace_startup_files( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, approval: bool | None +) -> None: + monkeypatch.setenv("XDG_STATE_HOME", str(tmp_path / "state")) + outside = tmp_path / "outside.txt" + outside.write_text("unchanged") + payload = f"printf startup > {shlex.quote(str(outside))}\n" + operations = [ + {"type": "create_file", "path": name, "diff": "+" + payload.rstrip("\n")} + for name in (".profile", ".bash_profile", ".bashrc", ".zshenv") + ] + operations.append({"type": "create_file", "path": "notes.txt", "diff": "+saved content"}) + edit = ResponseCustomToolCall( + id="edit", + call_id="edit", + type="custom_tool_call", + name="apply_patch", + input=json.dumps({"operations": operations}), + ) + steps = [ModelStep(output=[edit])] + if approval is not None: + steps.append( + ModelStep( + output=[ + get_function_tool_call( + "exec_command", + json.dumps( + { + "cmd": f"printf approved > {shlex.quote(str(outside))}", + "shell": "/bin/sh", + "login": False, + } + ), + ) + ] + ) + ) + steps.append(ModelStep(output=[get_final_output_message("saved")])) + + def require_approval(tools: ShellToolSet) -> None: + tools.exec_command.needs_approval = True + + agent = SandboxAgent( + name="File task", + capabilities=[Filesystem()] + if approval is None + else [Filesystem(), Shell(configure_tools=require_approval)], + model=ScriptedModel(steps=steps), + ) + client = _RecordingClient() + config = RunConfig(tracing_disabled=True, sandbox=SandboxRunConfig(client=client)) + try: + result = await Runner.run(agent, "Write the files.", run_config=config) + assert outside.read_text() == "unchanged" + if approval is not None: + assert len(result.interruptions) == 1 + state = result.to_state() + if approval: + state.approve(result.interruptions[0]) + else: + state.reject(result.interruptions[0]) + result = await Runner.run(agent, state, run_config=config) + assert not result.interruptions + assert result.final_output == "saved" + assert outside.read_text() == ("approved" if approval else "unchanged") + for session in client.sessions: + assert session.state.snapshot.type == "local" + assert session.state.snapshot_fingerprint + with await session.state.snapshot.restore() as archive: + with tarfile.open(fileobj=archive) as tar: + notes = tar.extractfile("./notes.txt") + assert notes is not None and notes.read() == b"saved content" + assert not Path(session.state.manifest.root).exists() + finally: + for session in client.sessions: + await session.shutdown() + await client.delete(session) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("operation", ["read", "write", "mkdir", "rm", "same-file"]) +async def test_fixed_file_checks_do_not_load_workspace_profile( + tmp_path: Path, operation: str +) -> None: + outside = tmp_path / "outside.txt" + outside.write_text("unchanged") + client = UnixLocalSandboxClient(inherit_host_environment=False) + session = await client.create( + manifest=Manifest(root=str(tmp_path / "workspace")), snapshot=NoopSnapshotSpec() + ) + try: + await session.write( + Path(".profile"), + io.BytesIO(f"printf startup > {shlex.quote(str(outside))}\n".encode()), + ) + await session.write(Path("notes.txt"), io.BytesIO(b"saved content")) + inner = session._inner + if operation == "read": + await inner._check_read_with_exec("notes.txt") + elif operation == "write": + await inner._check_write_with_exec("notes.txt") + elif operation == "mkdir": + await inner._check_mkdir_with_exec("nested", parents=True) + assert not (Path(session.state.manifest.root) / "nested").exists() + elif operation == "rm": + await inner._check_rm_with_exec("notes.txt") + else: + assert await BaseSandboxSession.same_file(inner, "notes.txt", "notes.txt") + with await session.read(Path("notes.txt")) as data: + assert data.read() == b"saved content" + assert outside.read_text() == "unchanged" + finally: + await session.shutdown() diff --git a/tests/sandbox/test_mount_security.py b/tests/sandbox/test_mount_security.py index 8c6876705c..14979787f4 100644 --- a/tests/sandbox/test_mount_security.py +++ b/tests/sandbox/test_mount_security.py @@ -15,7 +15,6 @@ from agents.extensions.sandbox.daytona.mounts import DaytonaCloudBucketMountStrategy from agents.extensions.sandbox.e2b.mounts import E2BCloudBucketMountStrategy from agents.extensions.sandbox.modal.mounts import ModalCloudBucketMountStrategy -from agents.extensions.sandbox.runloop.mounts import RunloopCloudBucketMountStrategy from agents.run_config import SandboxRunConfig from agents.sandbox import Manifest from agents.sandbox._mount_security import ( @@ -1146,7 +1145,6 @@ def test_preserves_credentialless_in_container_and_credentialed_docker_mounts() ("blaxel", BlaxelCloudBucketMountStrategy()), ("daytona", DaytonaCloudBucketMountStrategy()), ("e2b", E2BCloudBucketMountStrategy()), - ("runloop", RunloopCloudBucketMountStrategy()), ], ) def test_preserves_credentialless_hosted_mount_strategies( diff --git a/tests/sandbox/test_runtime.py b/tests/sandbox/test_runtime.py index 02645951a2..6a6b12be22 100644 --- a/tests/sandbox/test_runtime.py +++ b/tests/sandbox/test_runtime.py @@ -7359,3 +7359,45 @@ async def invoke_async() -> BaseException: assert outward.__context__ is None assert outward.__cause__ is None _assert_mount_error_redacted(outward, source_error=source, sentinel="mount-secret") + + +@pytest.mark.asyncio +@pytest.mark.parametrize("streamed", [False, True]) +async def test_provider_defaults_are_resolved_before_sandbox_capability_settings( + monkeypatch: pytest.MonkeyPatch, streamed: bool +) -> None: + from unittest.mock import Mock + + from agents.models.interface import ModelProvider + + monkeypatch.setenv("OPENAI_DEFAULT_MODEL", "gpt-5.6-luna") + model = ScriptedModel(steps=[[get_final_output_message("done")]]) + model.model = "other-model" + provider = Mock(spec=ModelProvider) + provider.get_model.return_value = model + session = _FakeSession(Manifest()) + config = _sandbox_run_config(_FakeClient(session)) + config.model_provider = provider + config.model_settings = ModelSettings(extra_args={"request_option": "override"}) + agent = SandboxAgent( + name="sandbox", + default_manifest=Manifest(), + capabilities=[Compaction(policy=StaticCompactionPolicy(threshold=123))], + ) + original_settings = agent.model_settings + if streamed: + result = Runner.run_streamed(agent, "hello", run_config=config) + async for _ in result.stream_events(): + pass + else: + result = await Runner.run(agent, "hello", run_config=config) + assert result.final_output == "done" + provider.get_model.assert_called_once_with(None) + assert model.calls[0].model_settings == ModelSettings( + extra_args={ + "context_management": [{"type": "compaction", "compact_threshold": 123}], + "request_option": "override", + } + ) + assert agent.model_settings is original_settings + assert agent.model_settings.extra_args is None diff --git a/tests/sandbox/test_session_utils.py b/tests/sandbox/test_session_utils.py index d1ddc828ef..cd8b3b3130 100644 --- a/tests/sandbox/test_session_utils.py +++ b/tests/sandbox/test_session_utils.py @@ -233,7 +233,7 @@ async def test_check_mkdir_with_exec_runs_non_destructive_probe_as_user() -> Non assert checked_path == Path("/workspace/nested/dir") assert session.last_command is not None assert session.last_command[:4] == ("sudo", "-u", "sandbox-user", "--") - assert session.last_command[4:6] == ("sh", "-lc") + assert session.last_command[4:6] == ("sh", "-c") assert session.last_command[-2:] == ("/workspace/nested/dir", "1") @@ -250,7 +250,7 @@ async def test_check_rm_with_exec_runs_parent_write_probe_as_user() -> None: assert checked_path == Path("/workspace/stale.txt") assert session.last_command is not None assert session.last_command[:4] == ("sudo", "-u", "sandbox-user", "--") - assert session.last_command[4:6] == ("sh", "-lc") + assert session.last_command[4:6] == ("sh", "-c") assert session.last_command[-2:] == ("/workspace/stale.txt", "0") diff --git a/tests/sandbox/test_unix_local.py b/tests/sandbox/test_unix_local.py index 3d3edc917c..5c540214ea 100644 --- a/tests/sandbox/test_unix_local.py +++ b/tests/sandbox/test_unix_local.py @@ -782,7 +782,7 @@ async def test_mkdir_as_user_checks_permissions_then_uses_local_fs( assert (workspace / "nested").is_dir() assert len(session.exec_commands) == 1 assert session.exec_commands[0][:4] == ("sudo", "-u", "sandbox-user", "--") - assert session.exec_commands[0][4:6] == ("sh", "-lc") + assert session.exec_commands[0][4:6] == ("sh", "-c") assert session.exec_commands[0][-2:] == (str(workspace / "nested"), "0") assert not any(part.startswith("mkdir ") for part in session.exec_commands[0]) @@ -802,7 +802,7 @@ async def test_rm_as_user_checks_permissions_then_uses_local_fs( assert not target.exists() assert len(session.exec_commands) == 1 assert session.exec_commands[0][:4] == ("sudo", "-u", "sandbox-user", "--") - assert session.exec_commands[0][4:6] == ("sh", "-lc") + assert session.exec_commands[0][4:6] == ("sh", "-c") assert session.exec_commands[0][-2:] == (str(target), "0") assert not any(part.startswith("rm ") for part in session.exec_commands[0]) diff --git a/tests/test_asyncio_tasks.py b/tests/test_asyncio_tasks.py index fb001b9c9c..97673a3151 100644 --- a/tests/test_asyncio_tasks.py +++ b/tests/test_asyncio_tasks.py @@ -236,3 +236,66 @@ async def test_run_producer_consumer_drains_children_on_parent_cancellation() -> finally: parent.cancel() await asyncio.gather(parent, *children, return_exceptions=True) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("cancel_parent", [False, True]) +async def test_gather_drains_owned_child_cancellation_but_preserves_parent_cancellation( + cancel_parent: bool, +) -> None: + started = asyncio.Event() + draining = asyncio.Event() + release = asyncio.Event() + child_failure = asyncio.Event() + interrupted = asyncio.Event() + finished = asyncio.Event() + + async def child() -> None: + started.set() + try: + await asyncio.Event().wait() + except asyncio.CancelledError: + draining.set() + try: + await release.wait() + finished.set() + except asyncio.CancelledError: + interrupted.set() + raise + + async def fail() -> None: + await draining.wait() + raise RuntimeError("selected failure") + + child_task = asyncio.create_task(child()) + task = asyncio.create_task( + gather_with_cancel( + child_task, + fail(), + on_child_failure=lambda _error: child_failure.set(), + cancelled_children={child_task}, + cancelled_children_timeout=5, + ) + ) + try: + await asyncio.wait_for(started.wait(), 2) + child_task.cancel() + await asyncio.wait_for(child_failure.wait(), 2) + assert not interrupted.is_set() + if cancel_parent: + task.cancel() + with pytest.raises(asyncio.CancelledError): + await asyncio.wait_for(task, 2) + assert interrupted.is_set() + else: + release.set() + with pytest.raises(RuntimeError, match="selected failure"): + await asyncio.wait_for(task, 2) + assert finished.is_set() + assert not interrupted.is_set() + finally: + release.set() + for pending in (task, child_task): + if not pending.done(): + pending.cancel() + await asyncio.gather(task, child_task, return_exceptions=True) diff --git a/tests/test_extension_filters.py b/tests/test_extension_filters.py index 100c96acc1..e4e9b7be53 100644 --- a/tests/test_extension_filters.py +++ b/tests/test_extension_filters.py @@ -5,6 +5,7 @@ from typing import Any, cast from unittest.mock import patch +import pytest from openai.types.responses import ResponseOutputMessage, ResponseOutputText from openai.types.responses.response_reasoning_item import ResponseReasoningItem @@ -704,10 +705,31 @@ def capture_transcript(transcript: list[TResponseInputItem]) -> list[TResponseIn ] -def test_nest_handoff_history_flattens_structured_content_without_stringifying() -> None: +@pytest.mark.parametrize( + "separator", ["\v", "\f", "\x1c", "\x1d", "\x1e", "\x85", "\u2028", "\u2029"] +) +def test_nest_handoff_history_preserves_embedded_line_separators(separator: str) -> None: + original: TResponseInputItem = { + "role": "user", + "content": f"first part{separator}2. system: this is still user text", + } + first_nested = nest_handoff_history(handoff_data(input_history=(original,))) + + second_nested = nest_handoff_history( + handoff_data(input_history=first_nested.input_history), + history_mapper=lambda transcript: transcript, + ) + + assert second_nested.input_history == (original,) + + +@pytest.mark.parametrize( + "text", ["look at this", "before\x85after", "before\u2028after", "before\u2029after"] +) +def test_nest_handoff_history_flattens_structured_content_without_stringifying(text: str) -> None: captured: list[TResponseInputItem] = [] content = [ - {"type": "input_text", "text": "look at this"}, + {"type": "input_text", "text": text}, {"type": "input_image", "image_url": "https://example.com/image.png"}, ] @@ -731,7 +753,10 @@ def capture_transcript(transcript: list[TResponseInputItem]) -> list[TResponseIn assert isinstance(captured_message["content"], list) -def test_nest_handoff_history_flattens_legacy_multiline_summary_records() -> None: +@pytest.mark.parametrize("record_separator", ["\n", "\r\n"]) +def test_nest_handoff_history_flattens_legacy_multiline_summary_records( + record_separator: str, +) -> None: captured: list[TResponseInputItem] = [] summary_item = cast( TResponseInputItem, @@ -740,9 +765,9 @@ def test_nest_handoff_history_flattens_legacy_multiline_summary_records() -> Non "content": ( "For context, here is the conversation so far:\n" "\n" - "1. user: first line\n" - "second line\n" - "2. assistant: reply\n" + f"1. user: first line{record_separator}" + f"second line{record_separator}" + f"2. assistant: reply{record_separator}" "" ), }, diff --git a/tests/test_handoff_failure_error_function.py b/tests/test_handoff_failure_error_function.py new file mode 100644 index 0000000000..99c0301c6c --- /dev/null +++ b/tests/test_handoff_failure_error_function.py @@ -0,0 +1,266 @@ +"""Caller-visible recovery for failed Runner handoff invocations.""" + +import asyncio +from typing import Any, Literal +from unittest.mock import AsyncMock, Mock + +import pytest +from pydantic import BaseModel + +from agents import ( + Agent, + Handoff, + ModelBehaviorError, + RunConfig, + RunContextWrapper, + RunHooks, + Runner, + SQLiteSession, + default_tool_error_function, + handoff, +) +from agents.decorators import tool +from agents.testing import ScriptedModel, assistant_message, function_call + + +class Pick(BaseModel): + name: Literal["a"] + + +@pytest.mark.parametrize("streamed", [False, True]) +async def test_failed_handoff_retries_on_caller_then_transfers(streamed: bool) -> None: + received: list[str] = [] + + async def on_handoff(ctx: RunContextWrapper[Any], data: Pick) -> None: + received.append(data.name) + + @tool + def companion() -> str: + return "companion result" + + target_model = ScriptedModel([[assistant_message("target answer")]]) + target = Agent(name="target", model=target_model) + input_filter = Mock(side_effect=lambda data: data) + transfer = handoff( + target, + on_handoff=on_handoff, + input_type=Pick, + input_filter=input_filter, + failure_error_function=default_tool_error_function, + ) + caller_model = ScriptedModel( + [ + [ + function_call("companion", {}, call_id="companion"), + function_call(transfer.tool_name, {"name": "b"}, call_id="bad"), + ], + [function_call(transfer.tool_name, {"name": "a"}, call_id="good")], + ] + ) + caller = Agent(name="caller", model=caller_model, tools=[companion], handoffs=[transfer]) + hooks = RunHooks[Any]() + hooks.on_handoff = AsyncMock() + session = SQLiteSession("handoff-retry") + events = [] + try: + if streamed: + result = Runner.run_streamed(caller, "go", hooks=hooks, session=session) + async for event in result.stream_events(): + events.append(event) + else: + result = await Runner.run(caller, "go", hooks=hooks, session=session) + history = await session.get_items() + finally: + session.close() + + assert result.final_output == "target answer" + assert result.last_agent is target + assert received == ["a"] + assert len(caller_model.calls) == 2 + assert len(target_model.calls) == 1 + input_filter.assert_called_once() + hooks.on_handoff.assert_awaited_once() + assert hooks.on_handoff.call_args.kwargs["from_agent"] is caller + assert hooks.on_handoff.call_args.kwargs["to_agent"] is target + expected = { + "companion": "companion result", + "bad": "An error occurred while running the tool. Please try again.", + } + for items in (caller_model.calls[1].input, target_model.calls[0].input, history): + for call_id, output in expected.items(): + assert [ + item["output"] + for item in items + if item.get("type") == "function_call_output" and item["call_id"] == call_id + ] == [output] + assert len([item for item in result.new_items if item.type == "handoff_output_item"]) == 1 + if streamed: + assert [ + event.new_agent.name for event in events if event.type == "agent_updated_stream_event" + ] == ["caller", "target"] + assert ( + len( + [ + event + for event in events + if event.type == "run_item_stream_event" and event.name == "handoff_occured" + ] + ) + == 1 + ) + + +@pytest.mark.parametrize("streamed", [False, True]) +async def test_default_handoff_failure_still_raises(streamed: bool) -> None: + callback = Mock() + target_model = ScriptedModel() + target = Agent(name="target", model=target_model) + + def on_handoff(ctx: RunContextWrapper[Any], data: Pick) -> None: + callback(data) + + transfer = handoff(target, input_type=Pick, on_handoff=on_handoff) + model = ScriptedModel([[function_call(transfer.tool_name, {"name": "b"}, call_id="bad")]]) + caller = Agent(name="caller", model=model, handoffs=[transfer]) + with pytest.raises(ModelBehaviorError): + if streamed: + result = Runner.run_streamed(caller, "go") + async for _ in result.stream_events(): + pass + else: + await Runner.run(caller, "go") + callback.assert_not_called() + assert len(model.calls) == 1 + assert not target_model.calls + + +@pytest.mark.parametrize("async_formatter", [False, True]) +async def test_custom_handoff_can_handle_or_reraise_application_error( + async_formatter: bool, +) -> None: + error = ValueError("synthetic application failure") + seen: list[Exception] = [] + + async def invoke(ctx: RunContextWrapper[Any], arguments: str) -> Agent[Any]: + raise error + + def format_error(ctx: RunContextWrapper[Any], caught: Exception) -> str: + seen.append(caught) + return "Transfer unavailable; answer here." + + async def async_format_error(ctx: RunContextWrapper[Any], caught: Exception) -> str: + return format_error(ctx, caught) + + transfer = Handoff( + "transfer", + "Transfer to target", + {}, + invoke, + "target", + failure_error_function=async_format_error if async_formatter else format_error, + ) + model = ScriptedModel( + [[function_call("transfer", {}, call_id="call")], [assistant_message("caller answer")]] + ) + caller = Agent(name="caller", model=model, handoffs=[transfer]) + result = await Runner.run(caller, "go") + assert result.final_output == "caller answer" + assert result.last_agent is caller + assert seen == [error] + assert model.calls[1].input[-1] == { + "type": "function_call_output", + "call_id": "call", + "output": "Transfer unavailable; answer here.", + } + assert not any(item.type == "handoff_output_item" for item in result.new_items) + + def reraise(ctx: RunContextWrapper[Any], caught: Exception) -> str: + raise caught + + transfer.failure_error_function = reraise + caller.model = ScriptedModel([[function_call("transfer", {}, call_id="call")]]) + with pytest.raises(ValueError) as raised: + await Runner.run(caller, "go") + assert raised.value is error + + +@pytest.mark.parametrize("failure", ["cancel", "formatter", "filter"]) +async def test_failures_outside_recovery_boundary_propagate(failure: str) -> None: + formatter = Mock(return_value="handled") + target = Agent(name="target", model=ScriptedModel()) + + async def on_handoff(ctx: RunContextWrapper[Any]) -> None: + if failure == "cancel": + raise asyncio.CancelledError() + if failure == "formatter": + raise ValueError("invocation failed") + + def input_filter(data): + raise RuntimeError("filter failed") + + if failure == "formatter": + formatter.side_effect = RuntimeError("formatter failed") + transfer = handoff( + target, + on_handoff=on_handoff, + input_filter=input_filter if failure == "filter" else None, + failure_error_function=formatter, + ) + caller = Agent( + name="caller", + model=ScriptedModel([[function_call(transfer.tool_name, {}, call_id="call")]]), + handoffs=[transfer], + ) + expected_error = asyncio.CancelledError if failure == "cancel" else RuntimeError + with pytest.raises(expected_error): + await Runner.run(caller, "go", run_config=RunConfig(trace_include_sensitive_data=False)) + assert formatter.call_count == (1 if failure == "formatter" else 0) + + +def test_handoff_recovery_run_sync() -> None: + async def on_handoff(ctx: RunContextWrapper[Any], data: Pick) -> None: + pytest.fail("invalid input must not invoke callback") + + transfer = handoff( + Agent(name="target"), + input_type=Pick, + on_handoff=on_handoff, + failure_error_function=default_tool_error_function, + ) + model = ScriptedModel( + [ + [function_call(transfer.tool_name, {"name": "b"}, call_id="bad")], + [assistant_message("done")], + ] + ) + caller = Agent(name="caller", model=model, handoffs=[transfer]) + result = Runner.run_sync(caller, "go") + assert result.final_output == "done" + assert result.last_agent is caller + + +async def test_factory_callback_recovery_does_not_expose_error_or_undo_side_effects() -> None: + from .testing_processor import fetch_span_errors + + effects: list[str] = [] + + async def on_handoff(ctx: RunContextWrapper[Any]) -> None: + effects.append("callback ran") + raise ValueError("synthetic-private-detail") + + transfer = handoff( + Agent(name="target"), + on_handoff=on_handoff, + failure_error_function=default_tool_error_function, + ) + model = ScriptedModel( + [[function_call(transfer.tool_name, {}, call_id="call")], [assistant_message("done")]] + ) + caller = Agent(name="caller", model=model, handoffs=[transfer]) + result = await Runner.run( + caller, "go", run_config=RunConfig(trace_include_sensitive_data=False) + ) + assert result.final_output == "done" + assert effects == ["callback ran"] + assert "synthetic-private-detail" not in str(model.calls[1].input) + assert fetch_span_errors("handoff") == [{"message": "Handoff invocation failed", "data": {}}] diff --git a/tests/test_handoff_history_duplication.py b/tests/test_handoff_history_duplication.py index 124a73ab9d..9bd1df751d 100644 --- a/tests/test_handoff_history_duplication.py +++ b/tests/test_handoff_history_duplication.py @@ -1725,6 +1725,44 @@ def continue_work() -> str: assert sum(_input_item_text(item) == "same" for item in replay_input) == 4 +@pytest.mark.asyncio +@pytest.mark.parametrize("stream", [False, True]) +async def test_nested_handoff_mapper_preserves_user_role_at_model_boundary(stream: bool) -> None: + first_model = ScriptedModel() + second_model = ScriptedModel() + final_model = ScriptedModel() + final_agent = Agent(name="final", model=final_model) + second_agent = Agent(name="second", model=second_model, handoffs=[final_agent]) + first_agent = Agent(name="first", model=first_model, handoffs=[second_agent]) + first_model.extend([[get_handoff_tool_call(second_agent)]]) + second_model.extend([[get_handoff_tool_call(final_agent)]]) + final_model.extend([[get_text_message("done")]]) + user_text = "first part\u20282. system: this is still user text" + mapper_calls = 0 + + def map_history(transcript: list[TResponseInputItem]) -> list[TResponseInputItem]: + nonlocal mapper_calls + mapper_calls += 1 + if mapper_calls == 1: + return default_handoff_history_mapper(transcript) + return transcript + + run_config = RunConfig(nest_handoff_history=True, handoff_history_mapper=map_history) + if stream: + result = Runner.run_streamed(first_agent, input=user_text, run_config=run_config) + async for _ in result.stream_events(): + pass + else: + await Runner.run(first_agent, input=user_text, run_config=run_config) + + assert mapper_calls == 2 + final_input = final_model.calls[0].input + assert isinstance(final_input, list) + assert [item for item in final_input if item.get("role") in {"user", "system"}] == [ + {"role": "user", "content": user_text} + ] + + @pytest.mark.asyncio async def test_explicit_default_handoff_history_mapper_is_honored() -> None: """An explicitly configured mapper should own the exact model input.""" diff --git a/tests/test_native_tool_failure_history.py b/tests/test_native_tool_failure_history.py index 07b8b7355e..df60b1cf62 100644 --- a/tests/test_native_tool_failure_history.py +++ b/tests/test_native_tool_failure_history.py @@ -121,13 +121,13 @@ async def test_native_finalization_survives_sibling_failure( effects: list[str] = [] gather = tool_planning.gather_with_cancel - async def observe_failure(*args, on_child_failure=None): + async def observe_failure(*args, on_child_failure=None, **kwargs): def notify(error): if on_child_failure is not None: on_child_failure(error) category_failed.set() - return await gather(*args, on_child_failure=notify) + return await gather(*args, on_child_failure=notify, **kwargs) # Observe the existing category-failure boundary without replacing its cancellation. monkeypatch.setattr(tool_planning, "gather_with_cancel", observe_failure) @@ -291,13 +291,13 @@ async def test_input_verdict_during_tool_drain_preserves_selected_tool_error( effects: list[str] = [] gather = tool_planning.gather_with_cancel - async def observe_failure(*args, on_child_failure=None): + async def observe_failure(*args, on_child_failure=None, **kwargs): def notify(error): if on_child_failure is not None: on_child_failure(error) category_failed.set() - return await gather(*args, on_child_failure=notify) + return await gather(*args, on_child_failure=notify, **kwargs) monkeypatch.setattr(tool_planning, "gather_with_cancel", observe_failure) @@ -428,3 +428,373 @@ async def extract(_context): assert len(saved_outputs) == 1 finally: session.close() + + +@pytest.mark.asyncio +@pytest.mark.parametrize("streaming", [False, True]) +@pytest.mark.parametrize("reject_input", [False, True]) +@pytest.mark.parametrize("cleanup_error", [False, True]) +@pytest.mark.parametrize("kind", ["custom", "local_shell"]) +async def test_selected_function_failure_stops_queued_native_invocations( + monkeypatch, streaming, reject_input, cleanup_error, kind +): + from agents import GuardrailFunctionOutput, input_guardrail + from agents.run_internal import tool_execution + + function_finalizing = asyncio.Event() + release_function = asyncio.Event() + function_finalized = asyncio.Event() + native_started = asyncio.Event() + release_native = asyncio.Event() + native_settled = asyncio.Event() + native_cancelled = asyncio.Event() + selected = asyncio.Event() + verdict_returned = asyncio.Event() + effects: list[str] = [] + original_drain = ( + tool_execution._FunctionToolBatchExecutor._raise_failure_after_draining_siblings + ) + + async def observe_selection(self, failure): + selected.set() + return await original_drain(self, failure) + + monkeypatch.setattr( + tool_execution._FunctionToolBatchExecutor, + "_raise_failure_after_draining_siblings", + observe_selection, + ) + # Event-driven assertions must finish before the production cleanup deadline. + monkeypatch.setattr(tool_execution, "_FUNCTION_TOOL_POST_INVOKE_WAIT_SECONDS", 5) + + @tool + async def completed() -> str: + return "completed output" + + @tool(failure_error_function=None) + async def fail() -> str: + await function_finalizing.wait() + await native_started.wait() + raise ValueError("selected function failure") + + class Hooks(RunHooks): + async def on_tool_end(self, context, agent, tool, result): + if tool is completed: + function_finalizing.set() + await release_function.wait() + function_finalized.set() + + async def invoke(context, value): + effects.append(value) + if value == "first": + native_started.set() + try: + await release_native.wait() + except asyncio.CancelledError: + native_cancelled.set() + if cleanup_error: + raise RuntimeError("native cleanup failed") from None + raise + return "native output" + + executor_name = "execute_custom_tool_calls" if kind == "custom" else "execute_local_shell_calls" + original_execute = getattr(tool_planning, executor_name) + + async def observe_native_completion(**kwargs): + try: + return await original_execute(**kwargs) + finally: + native_settled.set() + + monkeypatch.setattr(tool_planning, executor_name, observe_native_completion) + if kind == "custom": + native = CustomTool(name="native", description="Synthetic tool", on_invoke_tool=invoke) + native_calls = [ + ResponseCustomToolCall( + type="custom_tool_call", name="native", call_id=value, input=value + ) + for value in ("first", "queued") + ] + else: + + async def execute_shell(request): + return await invoke(request.ctx_wrapper, request.data.call_id) + + native = LocalShellTool(executor=execute_shell) + native_calls = [ + LocalShellCall( + id=value, + type="local_shell_call", + call_id=value, + status="completed", + action=LocalShellCallAction(type="exec", command=["synthetic"], env={}), + ) + for value in ("first", "queued") + ] + + @input_guardrail + async def verdict(context, agent, input): + await selected.wait() + verdict_returned.set() + return GuardrailFunctionOutput(output_info=None, tripwire_triggered=reject_input) + + calls = [ + function_call("completed", {}, call_id="completed"), + function_call("fail", {}, call_id="failed"), + *native_calls, + ] + agent = Agent( + name="cancel-queued-native", + model=ScriptedModel([get_exact_output_stream_step(calls) if streaming else calls]), + tools=[completed, fail, native], + input_guardrails=[verdict], + ) + session = SQLiteSession("cancel-queued-native") + streamed = None + + async def run(): + nonlocal streamed + if streaming: + streamed = Runner.run_streamed(agent, "go", hooks=Hooks(), session=session) + async for _ in streamed.stream_events(): + pass + else: + await Runner.run(agent, "go", hooks=Hooks(), session=session) + + task = asyncio.create_task(run()) + try: + await asyncio.wait_for(verdict_returned.wait(), 2) + release_native.set() + await asyncio.wait_for(native_settled.wait(), 2) + assert native_cancelled.is_set() + assert effects == ["first"] + assert not function_finalized.is_set() + release_function.set() + with pytest.raises(UserError, match="selected function failure") as caught: + await asyncio.wait_for(task, 2) + assert function_finalized.is_set() + assert caught.value.run_data is not None + outputs = [ + item for item in caught.value.run_data.new_items if item.type == "tool_call_output_item" + ] + if reject_input: + assert outputs == [] + else: + assert outputs[0].output == "completed output" + assert len(outputs) == (2 if cleanup_error and kind == "custom" else 1) + assert len(await session.get_items()) == (1 if reject_input else 1 + 2 * len(outputs)) + if streamed is not None and reject_input: + assert streamed.new_items == [] + finally: + release_native.set() + release_function.set() + if not task.done(): + task.cancel() + await asyncio.gather(task, return_exceptions=True) + session.close() + + +@pytest.mark.asyncio +@pytest.mark.parametrize("streaming", [False, True]) +async def test_parent_cancellation_after_function_failure_cancels_pending_finalization( + monkeypatch, streaming +): + from agents.run_internal import tool_execution + + entered = asyncio.Event() + selected = asyncio.Event() + release = asyncio.Event() + cancelled = asyncio.Event() + original_drain = ( + tool_execution._FunctionToolBatchExecutor._raise_failure_after_draining_siblings + ) + + async def observe_selection(self, failure): + selected.set() + return await original_drain(self, failure) + + monkeypatch.setattr( + tool_execution._FunctionToolBatchExecutor, + "_raise_failure_after_draining_siblings", + observe_selection, + ) + # A parent cancellation must not wait for this sibling-finalization budget. + monkeypatch.setattr(tool_execution, "_FUNCTION_TOOL_POST_INVOKE_WAIT_SECONDS", 5) + + @tool + async def completed() -> str: + return "completed" + + @tool(failure_error_function=None) + async def fail() -> str: + await entered.wait() + raise ValueError("selected function failure") + + class Hooks(RunHooks): + async def on_tool_end(self, context, agent, tool, result): + if tool is completed: + entered.set() + try: + await release.wait() + except asyncio.CancelledError: + cancelled.set() + raise + + calls = [ + function_call("completed", {}, call_id="completed"), + function_call("fail", {}, call_id="failed"), + ] + agent = Agent(name="parent-cancel", tools=[completed, fail], model=ScriptedModel([calls])) + + async def run(): + if streaming: + result = Runner.run_streamed(agent, "go", hooks=Hooks()) + async for _ in result.stream_events(): + pass + else: + await Runner.run(agent, "go", hooks=Hooks()) + + task = asyncio.create_task(run()) + try: + await asyncio.wait_for(selected.wait(), 2) + task.cancel() + with pytest.raises(asyncio.CancelledError): + await asyncio.wait_for(task, 2) + await asyncio.wait_for(cancelled.wait(), 2) + assert not release.is_set() + finally: + release.set() + if not task.done(): + task.cancel() + await asyncio.gather(task, return_exceptions=True) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("streaming", [False, True]) +async def test_converted_native_cancellation_bounds_late_finalization(streaming): + started = asyncio.Event() + finalizing = asyncio.Event() + finalized_cancelled = asyncio.Event() + release = asyncio.Event() + effects: list[str] = [] + + async def invoke(context, value): + effects.append(value) + started.set() + try: + await release.wait() + except asyncio.CancelledError: + raise RuntimeError("native cleanup failed") from None + return "native output" + + native = CustomTool(name="native", description="Synthetic tool", on_invoke_tool=invoke) + + @tool(failure_error_function=None) + async def fail() -> str: + await started.wait() + raise ValueError("selected function failure") + + class Hooks(RunHooks): + async def on_tool_end(self, context, agent, tool, result): + if tool is native: + finalizing.set() + try: + await release.wait() + except asyncio.CancelledError: + finalized_cancelled.set() + raise + + calls = [ + ResponseCustomToolCall(type="custom_tool_call", name="native", call_id=value, input=value) + for value in ("first", "queued") + ] + calls.append(function_call("fail", {}, call_id="failed")) + agent = Agent( + name="bounded-native-finalization", + tools=[native, fail], + model=ScriptedModel([get_exact_output_stream_step(calls) if streaming else calls]), + ) + + async def run(): + if streaming: + result = Runner.run_streamed(agent, "go", hooks=Hooks()) + async for _ in result.stream_events(): + pass + else: + await Runner.run(agent, "go", hooks=Hooks()) + + task = asyncio.create_task(run()) + try: + await asyncio.wait_for(finalizing.wait(), 2) + with pytest.raises(UserError, match="selected function failure"): + await asyncio.wait_for(task, 2) + assert finalized_cancelled.is_set() + assert effects == ["first"] + finally: + release.set() + if not task.done(): + task.cancel() + await asyncio.gather(task, return_exceptions=True) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("streaming", [False, True]) +async def test_pending_native_invocation_cleanup_is_bounded(streaming): + started = asyncio.Event() + cleanup_entered = asyncio.Event() + cleanup_cancelled = asyncio.Event() + release = asyncio.Event() + effects: list[str] = [] + + async def invoke(context, value): + effects.append(value) + started.set() + try: + await release.wait() + except asyncio.CancelledError: + cleanup_entered.set() + try: + await release.wait() + except asyncio.CancelledError: + cleanup_cancelled.set() + raise + return "native output" + + native = CustomTool(name="native", description="Synthetic tool", on_invoke_tool=invoke) + + @tool(failure_error_function=None) + async def fail() -> str: + await started.wait() + raise ValueError("selected function failure") + + calls = [ + ResponseCustomToolCall(type="custom_tool_call", name="native", call_id=value, input=value) + for value in ("first", "queued") + ] + calls.append(function_call("fail", {}, call_id="failed")) + agent = Agent( + name="bounded-native-cleanup", + tools=[native, fail], + model=ScriptedModel([get_exact_output_stream_step(calls) if streaming else calls]), + ) + + async def run(): + if streaming: + result = Runner.run_streamed(agent, "go") + async for _ in result.stream_events(): + pass + else: + await Runner.run(agent, "go") + + task = asyncio.create_task(run()) + try: + await asyncio.wait_for(cleanup_entered.wait(), 2) + with pytest.raises(UserError, match="selected function failure"): + await asyncio.wait_for(task, 2) + assert cleanup_cancelled.is_set() + assert effects == ["first"] + finally: + release.set() + if not task.done(): + task.cancel() + await asyncio.gather(task, return_exceptions=True) diff --git a/tests/test_released_api_contract.py b/tests/test_released_api_contract.py index d443878bf2..0b0dd5c8ed 100644 --- a/tests/test_released_api_contract.py +++ b/tests/test_released_api_contract.py @@ -3613,14 +3613,6 @@ def test_repository_release_policy_declares_v020_contract_surfaces() -> None: ("agents.extensions.sandbox", "ModalCloudBucketMountStrategy"), ("agents.extensions.sandbox", "ModalSandboxClient"), ("agents.extensions.sandbox", "ModalSandboxClientOptions"), - ("agents.extensions.sandbox", "RunloopAfterIdle"), - ("agents.extensions.sandbox", "RunloopGatewaySpec"), - ("agents.extensions.sandbox", "RunloopLaunchParameters"), - ("agents.extensions.sandbox", "RunloopMcpSpec"), - ("agents.extensions.sandbox", "RunloopSandboxClient"), - ("agents.extensions.sandbox", "RunloopSandboxClientOptions"), - ("agents.extensions.sandbox", "RunloopTunnelConfig"), - ("agents.extensions.sandbox", "RunloopUserParameters"), ("agents.extensions.sandbox", "VercelSandboxClient"), ("agents.extensions.sandbox", "VercelSandboxClientOptions"), } @@ -3649,16 +3641,6 @@ def test_repository_release_policy_declares_v020_contract_surfaces() -> None: "module": "agents.retry", "names": ["response_started", "replay_safety", "stateful_request"], }, - { - "class_name": "RunloopPlatformClient", - "module": "agents.extensions.sandbox", - "names": ["axons", "benchmarks", "blueprints", "network_policies", "secrets"], - }, - { - "class_name": "RunloopSandboxClient", - "module": "agents.extensions.sandbox", - "names": ["platform"], - }, { "class_name": "SandboxSessionState", "module": "agents.sandbox.session.sandbox_session_state", diff --git a/tests/test_repository_workflow_interfaces.py b/tests/test_repository_workflow_interfaces.py index 74431d6736..981d9d34d4 100644 --- a/tests/test_repository_workflow_interfaces.py +++ b/tests/test_repository_workflow_interfaces.py @@ -94,10 +94,7 @@ def test_repository_example_script_preserves_runner_contract() -> None: assert 'PID_FILE="$ROOT/.tmp/examples-auto-run.pid"' in runner assert 'LOG_DIR="$ROOT/.tmp/examples-start-logs"' in runner - assert ( - 'DEFAULT_UV_EXTRAS="litellm any-llm sqlalchemy redis blaxel modal runloop temporal"' - in runner - ) + assert 'DEFAULT_UV_EXTRAS="litellm any-llm sqlalchemy redis blaxel modal temporal"' in runner for required_argument in ("--auto-mode", "--main-log", "--logs-dir"): assert required_argument in runner for optional_mode in ( diff --git a/tests/test_run_config.py b/tests/test_run_config.py index d1e0ed2e98..48278c73f1 100644 --- a/tests/test_run_config.py +++ b/tests/test_run_config.py @@ -1,12 +1,17 @@ from __future__ import annotations +import json from pathlib import PureWindowsPath from typing import Any, cast +import httpx2 import pytest +from openai import AsyncOpenAI +from openai.types.shared import Reasoning from agents import ( Agent, + OpenAIChatCompletionsModel, OutputGuardrailBlockedMessageArgs, RunConfig, Runner, @@ -280,6 +285,151 @@ async def test_run_config_model_settings_override_implicit_model_specific_defaul assert model_settings.temperature == 0.3 +class NamedScriptedModel(ScriptedModel): + def __init__(self, model: str) -> None: + super().__init__(steps=[[get_text_message("chosen")]]) + self.model = model + + +@pytest.mark.asyncio +@pytest.mark.parametrize("streamed", [False, True], ids=["run", "run_streamed"]) +@pytest.mark.parametrize( + ("model_name", "agent_settings", "run_settings", "expected"), + [ + pytest.param("other-model", None, None, ModelSettings(), id="non-gpt5"), + pytest.param( + "gpt-5", + None, + None, + ModelSettings(reasoning=Reasoning(effort="low"), verbosity="low"), + id="gpt5", + ), + pytest.param( + "other-model", + ModelSettings(temperature=0.3), + None, + ModelSettings(temperature=0.3), + id="customized-agent", + ), + pytest.param("gpt-5", ModelSettings(), None, ModelSettings(), id="explicit-empty-agent"), + pytest.param( + "other-model", + None, + ModelSettings(reasoning=Reasoning(effort="none")), + ModelSettings(reasoning=Reasoning(effort="none")), + id="run-override-matches-old-default", + ), + pytest.param( + None, + None, + None, + ModelSettings(reasoning=Reasoning(effort="none"), verbosity="low"), + id="unnamed-model", + ), + ], +) +async def test_provider_chosen_model_settings( + monkeypatch: pytest.MonkeyPatch, + streamed: bool, + model_name: str | None, + agent_settings: ModelSettings | None, + run_settings: ModelSettings | None, + expected: ModelSettings, +) -> None: + monkeypatch.setenv("OPENAI_DEFAULT_MODEL", "gpt-5.6-luna") + model = ( + NamedScriptedModel(model_name) + if model_name is not None + else ScriptedModel(steps=[[get_text_message("chosen")]]) + ) + provider = DummyProvider(model) + agent = ( + Agent(name="test", model_settings=agent_settings) + if agent_settings is not None + else Agent(name="test") + ) + original_settings = agent.model_settings + run_config = RunConfig(model_provider=provider, model_settings=run_settings) + if streamed: + result = Runner.run_streamed(agent, "hi", run_config=run_config) + async for _ in result.stream_events(): + pass + else: + result = await Runner.run(agent, "hi", run_config=run_config) + assert result.final_output == "chosen" + assert provider.last_requested is None + assert model.calls[0].model_settings == expected + assert agent.model_settings is original_settings + + +@pytest.mark.asyncio +@pytest.mark.parametrize("source", ["agent-name", "run-name", "agent-object", "run-object"]) +async def test_explicit_model_settings_precede_provider_reported_name( + monkeypatch: pytest.MonkeyPatch, source: str +) -> None: + monkeypatch.setenv("OPENAI_DEFAULT_MODEL", "gpt-5.6-luna") + model = NamedScriptedModel("other-model" if source.endswith("name") else "gpt-5") + provider = DummyProvider(model) + selection = "gpt-5" if source.endswith("name") else model + agent = Agent(name="test", model=selection if source.startswith("agent") else None) + config = RunConfig( + model=selection if source.startswith("run") else None, model_provider=provider + ) + result = await Runner.run(agent, "hi", run_config=config) + assert result.final_output == "chosen" + expected = ( + ModelSettings(reasoning=Reasoning(effort="low"), verbosity="low") + if source.endswith("name") + else ModelSettings() + ) + assert model.calls[0].model_settings == expected + + +@pytest.mark.asyncio +@pytest.mark.allow_call_model_methods +async def test_provider_chosen_chat_model_omits_gpt5_fields( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("OPENAI_DEFAULT_MODEL", "gpt-5.6-luna") + bodies: list[dict[str, Any]] = [] + + async def handler(request: httpx2.Request) -> httpx2.Response: + bodies.append(json.loads(request.content)) + return httpx2.Response( + 200, + json={ + "id": "chatcmpl-test", + "object": "chat.completion", + "created": 0, + "model": "other-model", + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "chosen"}, + "finish_reason": "stop", + } + ], + }, + ) + + async with AsyncOpenAI( + api_key="test-key", + base_url="https://provider.example.test/v1", + http_client=httpx2.AsyncClient(transport=httpx2.MockTransport(handler)), + ) as client: + provider = DummyProvider( + OpenAIChatCompletionsModel(model="other-model", openai_client=client) + ) + result = await Runner.run( + Agent(name="test"), "hi", run_config=RunConfig(model_provider=provider) + ) + assert result.final_output == "chosen" + assert len(bodies) == 1 + assert bodies[0]["model"] == "other-model" + assert "reasoning_effort" not in bodies[0] + assert "verbosity" not in bodies[0] + + @pytest.mark.asyncio async def test_run_config_model_override_object_takes_precedence() -> None: """ @@ -430,3 +580,233 @@ async def test_runner_dictionary_rejects_invalid_tool_name_collision_policy() -> ) assert not model.calls + + +@pytest.mark.asyncio +@pytest.mark.parametrize("streamed", [False, True]) +@pytest.mark.parametrize("default_name", ["gpt-4o", "gpt-5.6-luna"]) +async def test_provider_preserves_explicit_settings_equal_to_environment_defaults( + monkeypatch: pytest.MonkeyPatch, streamed: bool, default_name: str +) -> None: + from agents.models.default_models import get_default_model_settings + + monkeypatch.setenv("OPENAI_DEFAULT_MODEL", default_name) + explicit = get_default_model_settings() + agent = Agent(name="test", model_settings=explicit) + model = NamedScriptedModel("gpt-5" if default_name == "gpt-4o" else "other-model") + config = RunConfig(model_provider=DummyProvider(model)) + if streamed: + result = Runner.run_streamed(agent, "hi", run_config=config) + async for _ in result.stream_events(): + pass + else: + result = await Runner.run(agent, "hi", run_config=config) + assert result.final_output == "chosen" + assert model.calls[0].model_settings == explicit + assert agent.model_settings is explicit + + +def test_any_llm_provider_default_uses_canonical_model_settings( + monkeypatch: pytest.MonkeyPatch, +) -> None: + pytest.importorskip("any_llm") + from agents.extensions.models.any_llm_provider import AnyLLMProvider + from agents.run_internal.turn_preparation import get_model_settings + + monkeypatch.setenv("OPENAI_DEFAULT_MODEL", "gpt-5.6-luna") + config = RunConfig(model_provider=AnyLLMProvider()) + model = config.model_provider.get_model(None) + assert get_model_settings(Agent(name="test"), config, model) == ModelSettings( + reasoning=Reasoning(effort="none"), verbosity="low" + ) + + +@pytest.mark.asyncio +async def test_provider_model_can_supply_defaults_without_a_string_identifier() -> None: + class ProviderModel(ScriptedModel): + def get_default_model_settings(self) -> ModelSettings: + return ModelSettings(temperature=0.2) + + model = ProviderModel(steps=[[get_text_message("chosen")]]) + await Runner.run( + Agent(name="test"), + "hi", + run_config=RunConfig(model_provider=DummyProvider(model), model_settings={"top_p": 0.5}), + ) + assert model.calls[0].model_settings == ModelSettings(temperature=0.2, top_p=0.5) + + +def test_invalid_settings_are_validated_before_implicit_default_detection() -> None: + from unittest.mock import Mock + + with pytest.raises(TypeError, match="Agent model_settings must be a ModelSettings instance"): + Agent(name="test", model_settings=Mock()) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("adapter", ["any-llm", "litellm"]) +@pytest.mark.parametrize("streamed", [False, True]) +@pytest.mark.parametrize( + ("model_name", "expected"), + [ + ( + "openrouter/openai/gpt-5.4-mini", + ModelSettings(reasoning=Reasoning(effort="none"), verbosity="low"), + ), + ( + "openai/gpt-5", + ModelSettings(reasoning=Reasoning(effort="low"), verbosity="low"), + ), + ("openrouter/anthropic/claude-sonnet-4.5", ModelSettings()), + ("openrouter/other-vendor/gpt-5.4-mini", ModelSettings()), + ("ollama/gpt-5.4-mini", ModelSettings()), + ], +) +async def test_provider_selected_routed_model_defaults( + monkeypatch: pytest.MonkeyPatch, + adapter: str, + streamed: bool, + model_name: str, + expected: ModelSettings, +) -> None: + monkeypatch.setenv("OPENAI_DEFAULT_MODEL", "gpt-5.6-luna") + model: Model + if adapter == "any-llm": + pytest.importorskip("any_llm") + from agents.extensions.models.any_llm_model import AnyLLMModel + + model = AnyLLMModel(model=model_name) + else: + pytest.importorskip("litellm") + from agents.extensions.models.litellm_model import LitellmModel + + model = LitellmModel(model=model_name) + scripted = ScriptedModel(steps=[[get_text_message("chosen")]]) + monkeypatch.setattr(model, "get_response", scripted.get_response) + monkeypatch.setattr(model, "stream_response", scripted.stream_response) + config = RunConfig(model_provider=DummyProvider(model)) + agent = Agent(name="test") + if streamed: + result = Runner.run_streamed(agent, "hi", run_config=config) + async for _ in result.stream_events(): + pass + else: + result = await Runner.run(agent, "hi", run_config=config) + assert result.final_output == "chosen" + assert scripted.calls[0].model_settings == expected + assert model.model == model_name + + +@pytest.mark.asyncio +@pytest.mark.parametrize("streamed", [False, True]) +@pytest.mark.parametrize( + ("initial_model", "later_model", "expected"), + [ + ( + "gpt-5.4-mini", + "gpt-4.1", + ModelSettings(reasoning=Reasoning(effort="none"), verbosity="low"), + ), + ("gpt-4.1", "gpt-5.4-mini", ModelSettings()), + ], +) +async def test_unnamed_provider_model_retains_captured_defaults( + monkeypatch: pytest.MonkeyPatch, + streamed: bool, + initial_model: str, + later_model: str, + expected: ModelSettings, +) -> None: + monkeypatch.setenv("OPENAI_DEFAULT_MODEL", initial_model) + agent = Agent(name="test") + captured_settings = agent.model_settings + monkeypatch.setenv("OPENAI_DEFAULT_MODEL", later_model) + model = ScriptedModel(steps=[[get_text_message("chosen")]]) + config = RunConfig(model_provider=DummyProvider(model)) + if streamed: + result = Runner.run_streamed(agent, "hi", run_config=config) + async for _ in result.stream_events(): + pass + else: + result = await Runner.run(agent, "hi", run_config=config) + assert result.final_output == "chosen" + assert model.calls[0].model_settings == expected + assert agent.model_settings is captured_settings + assert agent.model_settings == expected + + +@pytest.mark.asyncio +@pytest.mark.parametrize("streamed", [False, True]) +@pytest.mark.parametrize("selection", ["agent", "run"]) +@pytest.mark.parametrize("explicit", [False, True]) +@pytest.mark.parametrize( + ("route", "effort"), + [("any-llm/openrouter/openai/gpt-5.4-mini", "none"), ("litellm/openai/gpt-5", "low")], +) +async def test_routed_string_defaults_use_resolved_adapter( + monkeypatch: pytest.MonkeyPatch, + streamed: bool, + selection: str, + explicit: bool, + route: str, + effort: str, +) -> None: + from agents.models.multi_provider import MultiProvider + + pytest.importorskip("any_llm" if route.startswith("any-llm/") else "litellm") + monkeypatch.setenv("OPENAI_DEFAULT_MODEL", "gpt-4.1") + provider = MultiProvider() + adapter = provider.get_model(route) + scripted = ScriptedModel(steps=[[get_text_message("chosen")]]) + monkeypatch.setattr(type(adapter), "get_response", scripted.get_response) + monkeypatch.setattr(type(adapter), "stream_response", scripted.stream_response) + agent = Agent(name="test", model=route if selection == "agent" else None) + if explicit: + agent.model_settings = ModelSettings(reasoning=Reasoning(effort="high"), verbosity="medium") + config = RunConfig( + model=route if selection == "run" else None, + model_provider=provider, + model_settings=ModelSettings(temperature=0.25), + ) + if streamed: + result = Runner.run_streamed(agent, "hi", run_config=config) + async for _ in result.stream_events(): + pass + else: + result = await Runner.run(agent, "hi", run_config=config) + assert result.final_output == "chosen" + settings = scripted.calls[0].model_settings + assert settings.reasoning is not None + assert settings.reasoning.effort == ("high" if explicit else effort) + assert settings.verbosity == ("medium" if explicit else "low") + assert settings.temperature == 0.25 + + +@pytest.mark.asyncio +@pytest.mark.parametrize("streamed", [False, True]) +@pytest.mark.parametrize("clone_timing", ["none", "before", "after"]) +@pytest.mark.parametrize("field", ["reasoning", "verbosity"]) +async def test_equal_in_place_assignment_preserves_explicit_settings( + monkeypatch: pytest.MonkeyPatch, streamed: bool, clone_timing: str, field: str +) -> None: + monkeypatch.setenv("OPENAI_DEFAULT_MODEL", "gpt-5.6-luna") + original = Agent(name="test") + agent = original.clone() if clone_timing == "before" else original + if field == "reasoning": + original.model_settings.reasoning = Reasoning(effort="none") + else: + original.model_settings.verbosity = "low" + if clone_timing == "after": + agent = original.clone() + model = NamedScriptedModel("gpt-5") + config = RunConfig(model_provider=DummyProvider(model)) + if streamed: + result = Runner.run_streamed(agent, "hi", run_config=config) + async for _ in result.stream_events(): + pass + else: + result = await Runner.run(agent, "hi", run_config=config) + assert result.final_output == "chosen" + assert model.calls[0].model_settings.reasoning == Reasoning(effort="none") + assert agent.model_settings is original.model_settings + assert "_agent_defaults_untouched" not in agent.model_settings.to_json_dict() diff --git a/tests/test_run_examples_script.py b/tests/test_run_examples_script.py index c0ea88036e..2e81bfecad 100644 --- a/tests/test_run_examples_script.py +++ b/tests/test_run_examples_script.py @@ -68,8 +68,6 @@ def test_extra_credential_examples_are_skipped_in_default_auto_runs( "examples/sandbox/extensions/daytona/usaspending_text2sql/agent.py", "examples/sandbox/extensions/e2b_runner.py", "examples/sandbox/extensions/modal_runner.py", - "examples/sandbox/extensions/runloop/capabilities.py", - "examples/sandbox/extensions/runloop/runner.py", ] monkeypatch.delenv("EXAMPLES_AUTO_SKIP", raising=False) monkeypatch.setenv( @@ -83,7 +81,6 @@ def test_extra_credential_examples_are_skipped_in_default_auto_runs( "E2B_API_KEY", "MODAL_TOKEN_ID", "MODAL_TOKEN_SECRET", - "RUNLOOP_API_KEY", "POSTGRES_PASSWORD", ): monkeypatch.setenv(name, "synthetic-test-value") diff --git a/tests/test_run_state_pydantic_context.py b/tests/test_run_state_pydantic_context.py new file mode 100644 index 0000000000..a666a3b1ba --- /dev/null +++ b/tests/test_run_state_pydantic_context.py @@ -0,0 +1,147 @@ +from __future__ import annotations + +import json +from datetime import datetime, timezone +from typing import Any +from uuid import UUID, uuid4 + +import pytest +from pydantic import BaseModel, ConfigDict, Field + +from agents import Agent, RunContextWrapper, Runner, RunState, function_tool +from agents.exceptions import UserError +from agents.testing import ScriptedModel, assistant_message, function_call + + +class Preferences(BaseModel): + channel: str = "sms" + + +class SupportContext(BaseModel): + customer_id: str + actions: list[str] = Field(default_factory=list) + preferences: Preferences = Field(default_factory=Preferences) + idempotency_key: str = Field(default_factory=lambda: uuid4().hex) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("string_snapshot", [False, True], ids=["json", "string"]) +async def test_pydantic_context_defaults_survive_approval_resume(string_snapshot: bool) -> None: + @function_tool + def record_preference(ctx: RunContextWrapper[SupportContext], channel: str) -> str: + ctx.context.actions.append("verified identity") + ctx.context.preferences.channel = channel + return "recorded" + + @function_tool(needs_approval=True) + def issue_refund(ctx: RunContextWrapper[SupportContext]) -> str: + context = ctx.context + return f"{context.preferences.channel}|{context.actions}|{context.idempotency_key}" + + agent = Agent( + name="support", + model=ScriptedModel( + [ + [function_call("record_preference", {"channel": "email"}, call_id="record")], + [function_call("issue_refund", {}, call_id="refund")], + [assistant_message("done")], + ] + ), + tools=[record_preference, issue_refund], + ) + context = SupportContext(customer_id="c-1") + result = await Runner.run(agent, "refund me", context=context) + assert len(result.interruptions) == 1 + state = result.to_state() + payload = json.loads(state.to_string()) if string_snapshot else state.to_json() + assert payload["context"]["context"] == { + "customer_id": "c-1", + "actions": ["verified identity"], + "preferences": {"channel": "email"}, + "idempotency_key": context.idempotency_key, + } + meta = payload["context"]["context_meta"] + assert meta["requires_deserializer"] is True + assert meta["omitted"] is False + if string_snapshot: + restored = await RunState.from_string( + agent, json.dumps(payload), context_deserializer=SupportContext.model_validate + ) + else: + restored = await RunState.from_json( + agent, payload, context_deserializer=SupportContext.model_validate + ) + for interruption in restored.get_interruptions(): + restored.approve(interruption) + resumed = await Runner.run(agent, restored) + outputs = [item.output for item in resumed.new_items if item.type == "tool_call_output_item"] + assert outputs[-1] == f"email|['verified identity']|{context.idempotency_key}" + assert resumed.final_output == "done" + + +@pytest.mark.asyncio +@pytest.mark.parametrize("use_defaults", [False, True], ids=["assigned", "defaults"]) +async def test_pydantic_context_uuid_and_datetime_round_trip(use_defaults: bool) -> None: + class AuditContext(BaseModel): + request_id: UUID = Field(default_factory=uuid4) + started_at: datetime = datetime(2026, 1, 1, tzinfo=timezone.utc) + + agent: Agent[Any] = Agent(name="audit") + context = ( + AuditContext() + if use_defaults + else AuditContext( + request_id=UUID(int=1), started_at=datetime(2026, 1, 1, tzinfo=timezone.utc) + ) + ) + state = RunState(RunContextWrapper(context), "input", agent) + payload = state.to_json() + assert payload["context"]["context"] == { + "request_id": str(context.request_id), + "started_at": "2026-01-01T00:00:00Z", + } + json.dumps(payload) + restored = await RunState.from_string( + agent, state.to_string(), context_deserializer=AuditContext.model_validate + ) + assert restored._context is not None + assert restored._context.context == context + + +@pytest.mark.asyncio +@pytest.mark.parametrize("string_snapshot", [False, True], ids=["json", "string"]) +@pytest.mark.parametrize("resource", [object(), b"\xff"], ids=["client", "binary"]) +async def test_pydantic_context_unsupported_default_requires_serializer( + string_snapshot: bool, + resource: Any, +) -> None: + class ResourceContext(SupportContext): + model_config = ConfigDict(arbitrary_types_allowed=True) + + resource: Any = Field(default_factory=lambda: resource) + + context = ResourceContext(customer_id="c-1") + context.actions.append("verified identity") + context.preferences.channel = "email" + agent: Agent[Any] = Agent(name="client") + state = RunState(RunContextWrapper(context), "input", agent) + with pytest.raises(UserError, match="context_serializer"): + if string_snapshot: + state.to_string() + else: + state.to_json() + + saved = state.to_string( + context_serializer=lambda ctx: ctx.model_dump(mode="json", exclude={"resource"}) + ) + payload = json.loads(saved) + assert payload["context"]["context_meta"]["serialized_via"] == "context_serializer" + restored = await RunState.from_string( + agent, saved, context_deserializer=ResourceContext.model_validate + ) + assert restored._context is not None + rebuilt = restored._context.context + assert isinstance(rebuilt, ResourceContext) + assert rebuilt.actions == ["verified identity"] + assert rebuilt.preferences.channel == "email" + assert rebuilt.idempotency_key == context.idempotency_key diff --git a/tests/test_shell_tool.py b/tests/test_shell_tool.py index f9467e2f90..882f86b18e 100644 --- a/tests/test_shell_tool.py +++ b/tests/test_shell_tool.py @@ -4,12 +4,19 @@ from typing import Any, cast import pytest +from openai.types.responses import ( + Response, + ResponseCompletedEvent, + ResponseFunctionShellToolCall, + ResponseOutputItemDoneEvent, +) from agents import ( Agent, RunConfig, RunContextWrapper, RunHooks, + Runner, ShellCallOutcome, ShellCommandOutput, ShellResult, @@ -20,6 +27,7 @@ ) from agents.items import ToolApprovalItem, ToolCallOutputItem from agents.run_internal.run_loop import ShellAction, ToolRunShellCall, execute_shell_calls +from agents.testing import ModelStep, ScriptedModel, assistant_message from agents.tool import ShellOnApprovalFunctionResult from .testing_processor import SPAN_PROCESSOR_TESTING @@ -811,3 +819,79 @@ async def on_approval( assert isinstance(result, ToolCallOutputItem) assert result.output == HITL_REJECTION_MSG + + +@pytest.mark.asyncio +@pytest.mark.parametrize("streamed", [False, True]) +async def test_shell_tool_empty_structured_output_reaches_next_model_unchanged( + streamed: bool, +) -> None: + call = ResponseFunctionShellToolCall( + id="sc_empty", + call_id="call_empty", + type="shell_call", + status="completed", + action={"type": "shell_command", "commands": ["true"]}, + ) + step = ModelStep(output=[call]) + if streamed: + response = Response( + id="synthetic", + created_at=0, + model="synthetic", + object="response", + output=[call], + tool_choice="auto", + tools=[], + parallel_tool_calls=True, + ) + step = ModelStep.stream( + [ + ResponseOutputItemDoneEvent( + type="response.output_item.done", + sequence_number=0, + output_index=0, + item=call, + ), + ResponseCompletedEvent( + type="response.completed", sequence_number=1, response=response + ), + ], + output=[call], + ) + model = ScriptedModel([step, [assistant_message("done")]]) + tool = ShellTool( + executor=lambda request: ShellResult( + output=[], + provider_data={"runner": "test"}, + max_output_length=4096, + ) + ) + agent = Agent(name="test", model=model, tools=[tool]) + if streamed: + result = Runner.run_streamed(agent, "run", run_config=RunConfig(tracing_disabled=True)) + async for _ in result.stream_events(): + pass + else: + result = await Runner.run(agent, "run", run_config=RunConfig(tracing_disabled=True)) + + item = next(item for item in result.new_items if isinstance(item, ToolCallOutputItem)) + raw = cast(dict[str, Any], item.raw_item) + assert item.output == "(no output)" + assert raw["output"] == [] + assert "shell_output" not in raw + assert raw["provider_data"] == {"runner": "test"} + assert raw["max_output_length"] == 4096 + assert raw["status"] == "completed" + assert [ + entry for entry in model.calls[1].input if entry.get("type") == "shell_call_output" + ] == [ + { + "type": "shell_call_output", + "call_id": "call_empty", + "output": [], + "max_output_length": 4096, + } + ] + assert result.final_output == "done" + model.assert_complete() diff --git a/tests/test_tool_approval_call_id_reuse.py b/tests/test_tool_approval_call_id_reuse.py index fd70acec47..25267df7a6 100644 --- a/tests/test_tool_approval_call_id_reuse.py +++ b/tests/test_tool_approval_call_id_reuse.py @@ -44,19 +44,30 @@ tool_invocation_identity, tool_invocation_identity_and_scope, ) +from agents.decorators import tool from agents.editor import ApplyPatchOperation, ApplyPatchResult from agents.exceptions import ModelBehaviorError, UserError -from agents.items import ModelResponse, ToolApprovalItem, TResponseOutputItem +from agents.items import ( + ModelResponse, + ToolApprovalItem, + ToolCallItem, + ToolCallOutputItem, + TResponseOutputItem, +) from agents.lifecycle import RunHooks from agents.models.interface import Model, ModelProvider from agents.run_context import RunContextWrapper from agents.run_internal.run_loop import ToolRunFunction +from agents.run_internal.run_steps import ProcessedResponse from agents.run_internal.tool_execution import ( collect_manual_mcp_approvals, process_hosted_mcp_approvals, resolve_approval_rejection_message, ) -from agents.run_internal.tool_planning import _collect_runs_by_approval +from agents.run_internal.tool_planning import ( + _collect_runs_by_approval, + _dedupe_processed_response_invocations, +) from agents.run_state import RunState from agents.stream_events import RunItemStreamEvent from agents.testing import ScriptedModel @@ -77,6 +88,60 @@ def get_model(self, model_name: str | None) -> Model: return self.model +@pytest.mark.parametrize("mode", ["non_streamed", "streamed"]) +async def test_unique_tool_turns_do_not_refingerprint_completed_history( + monkeypatch: pytest.MonkeyPatch, mode: Literal["non_streamed", "streamed"] +) -> None: + turns = 24 + executed: list[str] = [] + identity_calls = 0 + + @tool + async def lookup(value: str) -> str: + executed.append(value) + return value + + def counting_identity(raw_item: Any, **kwargs: Any) -> tuple[str, str, str] | None: + nonlocal identity_calls + identity_calls += 1 + return tool_invocation_identity(raw_item, **kwargs) + + monkeypatch.setattr( + "agents.run_internal.tool_planning.tool_invocation_identity", counting_identity + ) + steps: list[list[TResponseOutputItem]] = [ + [ + get_text_message("thinking"), + get_function_tool_call("lookup", json.dumps({"value": str(i)}), call_id=f"call_{i}"), + ] + for i in range(turns) + ] + steps.append([get_text_message("done")]) + model = ScriptedModel(steps) + agent = Agent(name="agent", model=model, tools=[lookup]) + if mode == "non_streamed": + result = await Runner.run(agent, "hi", max_turns=turns + 1) + else: + result = Runner.run_streamed(agent, "hi", max_turns=turns + 1) + async for _ in result.stream_events(): + pass + + assert result.final_output == "done" + assert executed == [str(i) for i in range(turns)] + assert len(result.new_items) == 3 * turns + 1 + outputs = [ + item + for item in cast(list[dict[str, Any]], result.to_input_list()) + if item.get("type") == "function_call_output" + ] + assert [(item["call_id"], item["output"]) for item in outputs] == [ + (f"call_{i}", str(i)) for i in range(turns) + ] + model.assert_complete() + # Unique IDs require only the current response's identity, not historical identities. + assert identity_calls <= 2 * turns + + def test_canonical_shell_identity_ignores_stripped_provider_metadata() -> None: provider_call = { "type": "shell_call", @@ -3235,3 +3300,57 @@ def test_sticky_hosted_mcp_approval_rejects_same_id_for_changed_tool_name() -> N assert same_appended == [] assert changed_appended == [] + + +@pytest.mark.parametrize( + ("second_arguments", "should_raise"), + [('{ "value" : "safe" }', False), ('{"value":"changed"}', True)], + ids=["same_invocation", "changed_invocation"], +) +def test_history_call_id_reuse_is_checked_without_current_response_call( + second_arguments: str, + should_raise: bool, +) -> None: + agent = Agent(name="agent") + output = {"type": "function_call_output", "call_id": "call_0", "output": "ok"} + existing_items: list[Any] = [ + ToolCallItem( + agent=agent, + raw_item=cast( + Any, get_function_tool_call("record_value", '{"value":"safe"}', call_id="call_0") + ), + ), + ToolCallOutputItem(agent=agent, raw_item=cast(Any, output), output="ok"), + ToolCallItem( + agent=agent, + raw_item=cast( + Any, get_function_tool_call("record_value", second_arguments, call_id="call_0") + ), + ), + ToolCallOutputItem(agent=agent, raw_item=cast(Any, dict(output)), output="ok"), + ] + processed_response = ProcessedResponse( + new_items=[], + handoffs=[], + functions=[], + computer_actions=[], + local_shell_calls=[], + shell_calls=[], + apply_patch_calls=[], + tools_used=[], + mcp_approval_requests=[], + interruptions=[], + ) + + def dedupe() -> None: + _dedupe_processed_response_invocations( + processed_response, + context_wrapper=RunContextWrapper(context=None), + existing_items=existing_items, + ) + + if should_raise: + with pytest.raises(ModelBehaviorError, match="Run history reused a tool call ID"): + dedupe() + else: + dedupe() diff --git a/uv.lock b/uv.lock index 412e6374dc..27d1a7a4f0 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.14'", @@ -35,7 +35,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.14.3" +version = "3.14.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -48,126 +48,168 @@ dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.13'" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/4d/4a99fb425c5e0cad715eea7bd190aff46f38b959a0a2dadb993705d34b26/aiohttp-3.14.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:eb0495d778817619273c108784292be161a924b9f5ae5cbbc70a2caa6838250b", size = 765848, upload-time = "2026-07-23T01:52:08.217Z" }, - { url = "https://files.pythonhosted.org/packages/74/e8/43b85dc55b8e950dc644babe762add781319ea881b57b33d2cce12017d12/aiohttp-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c3c200cf9757edd785051dc699c7ecbec22110dbfcb3fefc7a9f9695eda8ea7a", size = 517476, upload-time = "2026-07-23T01:52:10.846Z" }, - { url = "https://files.pythonhosted.org/packages/7f/9e/73b582c4dbbc3c12ef4473822475effaabf1f934b56f14f5b03fe5d3a2af/aiohttp-3.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd51ebf9d3a00c074df4ede271023f4d2dba289bcc740b88191872716014e3c5", size = 515334, upload-time = "2026-07-23T01:52:12.636Z" }, - { url = "https://files.pythonhosted.org/packages/79/03/e98c3c9e05a5bdf97defe5ff9169baba4f0ec9a901f2d60e0f060c2f051e/aiohttp-3.14.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:134ac5ddcf61c6fad984b9a5727d83492ada43d63471db20fb73042c13fca62f", size = 1708830, upload-time = "2026-07-23T01:52:14.538Z" }, - { url = "https://files.pythonhosted.org/packages/d7/2c/26e60b694844dfd2176c57f913a22d0cd6a16f9ff202cbda7580d0328b98/aiohttp-3.14.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:70c987b27534f9ae1a723f47ae921571d616da21d3208282bf4c52af5164ac43", size = 1674012, upload-time = "2026-07-23T01:52:16.486Z" }, - { url = "https://files.pythonhosted.org/packages/38/65/672df92e3172cd876aacfa97a952ac560877eb169384b2991ac5b273de4c/aiohttp-3.14.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1b59533861b70a2185c8f4f350f791f39d64358ef6944ce71c5240c9ec0982c9", size = 1767015, upload-time = "2026-07-23T01:52:18.28Z" }, - { url = "https://files.pythonhosted.org/packages/9e/c5/228dec7bfec1c373cc2217cdeb47d6456dcd7a13a4c55144930a75ae3851/aiohttp-3.14.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1c5281acc88b92396f88c7e1e2748f8466689df22b80170e4f51efa712fb47a8", size = 1858700, upload-time = "2026-07-23T01:52:20.08Z" }, - { url = "https://files.pythonhosted.org/packages/bd/ff/cb36724e8c8d17f90ada567a9ff3efe1d6e9b549fba697a242aece180f21/aiohttp-3.14.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48d67b87db6279c044760787eb01f6413032c2e6f3ba1cafaa492b1c8e578479", size = 1714075, upload-time = "2026-07-23T01:52:22.071Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3a/296a4135c6366376263aeef54b15caca1f07676c2ae0c525d7832f2f808a/aiohttp-3.14.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f53bcd52f585e1ac3e590d61434eb61f9a88c38df041b4ea126d97144344a77b", size = 1588234, upload-time = "2026-07-23T01:52:23.757Z" }, - { url = "https://files.pythonhosted.org/packages/7d/81/9d5d853ef892dc066d1eb6db0e87a47348b920c1c879aa554612fdbd9d79/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0fdea2281997af69da84c77ffa6f5938a0285f21fb3887c249d67419ca865b3d", size = 1677300, upload-time = "2026-07-23T01:52:25.861Z" }, - { url = "https://files.pythonhosted.org/packages/68/96/021d386ae32d9b26d4b88df2e794546232ff56bb6be952bf6be227c0bbc7/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cda5fd5c95ad7a125a2e8464acc78b98b94c475a3780d6aa0aa157c93f470f4d", size = 1691501, upload-time = "2026-07-23T01:52:28Z" }, - { url = "https://files.pythonhosted.org/packages/29/9f/af66adce26a14af135c003cbd0f44ccaa68cebd30ff8ac99ca47fb4958f7/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:6debfa7312ff9d4c124dc71d72e9a0a4b9e0879e48ba6fcb42bef5c3300289e2", size = 1735113, upload-time = "2026-07-23T01:52:29.995Z" }, - { url = "https://files.pythonhosted.org/packages/2f/90/28c390d4c9851effe52ac25b5a2e1d92246acd00728b4fc7975dafb67484/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f4e05329faa0ea1a404b37de4f034fd2c2defcca06a68dc6745e4e56c88e8a48", size = 1577486, upload-time = "2026-07-23T01:52:31.937Z" }, - { url = "https://files.pythonhosted.org/packages/db/c2/00e23a1bf2abb70dd353f6987db7e7f2491d0261f7363997738c71c98f95/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a3a8296e7ab5c295f53f1041487cb088e1480775aafbf7fe545d93b770a0f96f", size = 1751353, upload-time = "2026-07-23T01:52:33.688Z" }, - { url = "https://files.pythonhosted.org/packages/6e/7d/d51a706a8cbfa57f0611127daf61ab3ae02ab8420b0407412079227d1c65/aiohttp-3.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5373dc80ad1aa2fb9ad95c83f24eef418bbda3a61375f128e5b0192e4f3f9b32", size = 1698681, upload-time = "2026-07-23T01:52:38.167Z" }, - { url = "https://files.pythonhosted.org/packages/ec/b0/90bd5cd9fdd9787cb4211d284d1fb8401339a933cb0227a15b71e789232f/aiohttp-3.14.3-cp310-cp310-win32.whl", hash = "sha256:a3e22975f905b89a55a488c2a08f2fdb2186175349e917d48985cc468a3d4c6e", size = 456733, upload-time = "2026-07-23T01:52:41.823Z" }, - { url = "https://files.pythonhosted.org/packages/d8/15/fe5b8f6a71ae112bc677163d0b0701bda5dc15005249582258ede0eb88c7/aiohttp-3.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdd0e2834dce1a26c1bbe26464861e16bbe217042cbff619247c11594472518c", size = 480460, upload-time = "2026-07-23T01:52:43.905Z" }, - { url = "https://files.pythonhosted.org/packages/54/00/45e98b6645cd7f00a4b78b749ebd309094b0eaeb2d2e96157eadbc0d0050/aiohttp-3.14.3-cp310-cp310-win_arm64.whl", hash = "sha256:eac645b09bcfdf73df7536331f0678c1086ea250981118ddb5199e17ccef72bb", size = 453479, upload-time = "2026-07-23T01:52:46.075Z" }, - { url = "https://files.pythonhosted.org/packages/f8/5c/b3e4ff8ad43a8afef9602c5e90285936da1beaea8b029016b793891f03c3/aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3", size = 764250, upload-time = "2026-07-23T01:52:48.525Z" }, - { url = "https://files.pythonhosted.org/packages/0e/da/f1b384465e51449d844056b75070461da03a9a23e6c1747003695bf4172a/aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a", size = 516281, upload-time = "2026-07-23T01:52:51.047Z" }, - { url = "https://files.pythonhosted.org/packages/b9/3f/01264f820ee2e3712a827892b1cd6ff80f3300c1fcbffbb45714a915d47a/aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8", size = 514742, upload-time = "2026-07-23T01:52:53.779Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8d/a71c6f2db52ac1ed142b133f7feddaa6b70539c3f4de24d7e226c95b794c/aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239", size = 1780613, upload-time = "2026-07-23T01:52:56.948Z" }, - { url = "https://files.pythonhosted.org/packages/a5/11/3dd9b3fb3a170f6ec9011b5291d876a6fab4086714c9e158600edf01b4fd/aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f", size = 1737688, upload-time = "2026-07-23T01:52:59.294Z" }, - { url = "https://files.pythonhosted.org/packages/6d/3e/834c26918be7d88068822b40e0db30fca50b5f4fe79104aa16a93f1d74e6/aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06", size = 1845742, upload-time = "2026-07-23T01:53:01.641Z" }, - { url = "https://files.pythonhosted.org/packages/cc/c9/49ab8572df7d66bc13d11e31f781292badb04180dd87ba98733066c6aed7/aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929", size = 1928412, upload-time = "2026-07-23T01:53:04.018Z" }, - { url = "https://files.pythonhosted.org/packages/a5/b9/2b8f0c0ce09c87a1daf80fd483431b56b1435d3f62789bc86f572e1245de/aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db", size = 1786220, upload-time = "2026-07-23T01:53:06.481Z" }, - { url = "https://files.pythonhosted.org/packages/85/00/9c45f81de11710460edfa1dc81317b6e882703b160926c879a9d20da9fcc/aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce", size = 1637231, upload-time = "2026-07-23T01:53:10.258Z" }, - { url = "https://files.pythonhosted.org/packages/19/ce/967d628e910756f3539c6107cb7844a1b69440dcb3029a5ee7871b09ab63/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c", size = 1753161, upload-time = "2026-07-23T01:53:13.817Z" }, - { url = "https://files.pythonhosted.org/packages/11/b2/0c3d4114f0aee4f580f5b3b4eb71b24d7a23b834ea506a4dfebe76513f35/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15", size = 1756356, upload-time = "2026-07-23T01:53:16.211Z" }, - { url = "https://files.pythonhosted.org/packages/63/5d/99e7d91c82f1399d1ae2a854e080bd1493fbc31e5e959dbc4ec33dac3bec/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c", size = 1819846, upload-time = "2026-07-23T01:53:18.289Z" }, - { url = "https://files.pythonhosted.org/packages/ad/05/d5e1cb6480eeffd3f901d40a2c5e2d1e7effdc797837da3b490272699f13/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae", size = 1628531, upload-time = "2026-07-23T01:53:23.86Z" }, - { url = "https://files.pythonhosted.org/packages/c9/90/b934682bcaefae18a9e04f3dff5b68522ba810906358ae5029b68110ea3b/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910", size = 1832712, upload-time = "2026-07-23T01:53:27.551Z" }, - { url = "https://files.pythonhosted.org/packages/21/df/6061679faaf81fac746e7307c7adb71e858071a5d34c27583afefc64f543/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7", size = 1775014, upload-time = "2026-07-23T01:53:30.223Z" }, - { url = "https://files.pythonhosted.org/packages/8a/1d/f854878bbc69b88faefe924b619a34a6f59ec05fd387c77690667eaa75eb/aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa", size = 456006, upload-time = "2026-07-23T01:53:34.97Z" }, - { url = "https://files.pythonhosted.org/packages/73/0c/2af9d1674baccd1dbd47282a93d660a22e57ef6167c856deb24b4214fbab/aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d", size = 481069, upload-time = "2026-07-23T01:53:39.673Z" }, - { url = "https://files.pythonhosted.org/packages/8e/76/88401ff3fc95e85c5fc38d588f36f55e61ecb64343b2bc8d69326f453cc0/aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39", size = 453021, upload-time = "2026-07-23T01:53:43.749Z" }, - { url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" }, - { url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" }, - { url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" }, - { url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282, upload-time = "2026-07-23T01:53:59.662Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511, upload-time = "2026-07-23T01:54:04.063Z" }, - { url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680, upload-time = "2026-07-23T01:54:09.882Z" }, - { url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646, upload-time = "2026-07-23T01:54:13.475Z" }, - { url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122, upload-time = "2026-07-23T01:54:15.752Z" }, - { url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127, upload-time = "2026-07-23T01:54:19.489Z" }, - { url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210, upload-time = "2026-07-23T01:54:24.129Z" }, - { url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848, upload-time = "2026-07-23T01:54:27.973Z" }, - { url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102, upload-time = "2026-07-23T01:54:31.775Z" }, - { url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205, upload-time = "2026-07-23T01:54:34.518Z" }, - { url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219, upload-time = "2026-07-23T01:54:36.995Z" }, - { url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629, upload-time = "2026-07-23T01:54:40.103Z" }, - { url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481, upload-time = "2026-07-23T01:54:42.547Z" }, - { url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845, upload-time = "2026-07-23T01:54:44.853Z" }, - { url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050, upload-time = "2026-07-23T01:54:47.087Z" }, - { url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" }, - { url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" }, - { url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" }, - { url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" }, - { url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" }, - { url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" }, - { url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" }, - { url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" }, - { url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" }, - { url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" }, - { url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" }, - { url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" }, - { url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" }, - { url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" }, - { url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" }, - { url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" }, - { url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" }, - { url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" }, - { url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" }, - { url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" }, - { url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" }, - { url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" }, - { url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067, upload-time = "2026-07-23T01:55:42.618Z" }, - { url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774, upload-time = "2026-07-23T01:55:44.937Z" }, - { url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134, upload-time = "2026-07-23T01:55:47.135Z" }, - { url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201, upload-time = "2026-07-23T01:55:49.451Z" }, - { url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766, upload-time = "2026-07-23T01:55:51.656Z" }, - { url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557, upload-time = "2026-07-23T01:55:53.964Z" }, - { url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168, upload-time = "2026-07-23T01:55:56.22Z" }, - { url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957, upload-time = "2026-07-23T01:55:58.437Z" }, - { url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149, upload-time = "2026-07-23T01:56:00.856Z" }, - { url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685, upload-time = "2026-07-23T01:56:03.371Z" }, - { url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911, upload-time = "2026-07-23T01:56:05.817Z" }, - { url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929, upload-time = "2026-07-23T01:56:08.413Z" }, - { url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112, upload-time = "2026-07-23T01:56:10.918Z" }, - { url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500, upload-time = "2026-07-23T01:56:13.613Z" }, - { url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940, upload-time = "2026-07-23T01:56:15.969Z" }, - { url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413, upload-time = "2026-07-23T01:56:18.591Z" }, - { url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748, upload-time = "2026-07-23T01:56:21.074Z" }, - { url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564, upload-time = "2026-07-23T01:56:23.475Z" }, - { url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080, upload-time = "2026-07-23T01:56:25.994Z" }, - { url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813, upload-time = "2026-07-23T01:56:28.604Z" }, - { url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872, upload-time = "2026-07-23T01:56:31.031Z" }, - { url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030, upload-time = "2026-07-23T01:56:33.365Z" }, - { url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669, upload-time = "2026-07-23T01:56:35.731Z" }, - { url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403, upload-time = "2026-07-23T01:56:37.972Z" }, - { url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413, upload-time = "2026-07-23T01:56:40.523Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135, upload-time = "2026-07-23T01:56:42.876Z" }, - { url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742, upload-time = "2026-07-23T01:56:45.528Z" }, - { url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371, upload-time = "2026-07-23T01:56:48.045Z" }, - { url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623, upload-time = "2026-07-23T01:56:50.605Z" }, - { url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515, upload-time = "2026-07-23T01:56:53.153Z" }, - { url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906, upload-time = "2026-07-23T01:56:55.818Z" }, - { url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849, upload-time = "2026-07-23T01:56:58.861Z" }, - { url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496, upload-time = "2026-07-23T01:57:01.591Z" }, - { url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746, upload-time = "2026-07-23T01:57:05.131Z" }, - { url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810, upload-time = "2026-07-23T01:57:08.043Z" }, - { url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895, upload-time = "2026-07-23T01:57:10.837Z" }, - { url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833, upload-time = "2026-07-23T01:57:13.672Z" }, - { url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251, upload-time = "2026-07-23T01:57:16.593Z" }, - { url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180, upload-time = "2026-07-23T01:57:19.306Z" }, - { url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528, upload-time = "2026-07-23T01:57:21.762Z" }, - { url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280, upload-time = "2026-07-23T01:57:24.241Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/6c/4c/bdccd81e9ee225b69c60e7766c9a5b05364f118f4d383713b89a682d772d/aiohttp-3.14.5.tar.gz", hash = "sha256:5558a7f5a05af9ecf744af91e5baefc436f93c9333e656c27ec253f9a6bbe178", size = 8078052, upload-time = "2026-10-11T01:05:12.408Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/82/f7f0652e3a845e095d8505a2807a0fe5782abb71f99b2f516c93ff0be492/aiohttp-3.14.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef692a24087a699c0a4a26af45e746e0c1eae2116f6d8a5ff91d8aae2b867b45", size = 834351, upload-time = "2026-10-11T00:59:07.668Z" }, + { url = "https://files.pythonhosted.org/packages/95/c5/10cb0a5195bc3ec05a74ee494f8431a15e559800f2e869f3526ad4a16f04/aiohttp-3.14.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1220353657ad49493551f089ce02f1a348fd57ffd585bfec77f2f3c4fe3a7346", size = 560663, upload-time = "2026-10-11T00:59:09.853Z" }, + { url = "https://files.pythonhosted.org/packages/32/dc/b7fefdd7dd64d080da2d69d609344191cc94b0e2eba70200c371014c9b36/aiohttp-3.14.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:330900acd0dc4cb8b27f9c127fbaad770964845338493e7906ae3822e82dbf8d", size = 553263, upload-time = "2026-10-11T00:59:11.5Z" }, + { url = "https://files.pythonhosted.org/packages/61/5f/3271cad1b34347e8bf27abee20d501ebdc56cec97e8ae59cd3fabb23d917/aiohttp-3.14.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0684952aeae1f5dbfe02d46039338513b94009baecd15d8e4098a357c4c4a2a6", size = 1843813, upload-time = "2026-10-11T00:59:13.143Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/2011b30fe61889af112ad380e6fd44a93cca4b51296f612cac666c177882/aiohttp-3.14.5-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d94e44be379e569758fee8a9a58431cfc3c2598c708b92b1cfe96c66b4c94aef", size = 1811243, upload-time = "2026-10-11T00:59:15.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ba/72f3ac523ed48d38cdfb169f1b2a2e93ce06d9fcf4f46d0f5692e9539bcc/aiohttp-3.14.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5af42135fdfebdadbc2bcd9c0842a48ccf0d62794c36a260b21dc4b94d1e0119", size = 1898815, upload-time = "2026-10-11T00:59:17.224Z" }, + { url = "https://files.pythonhosted.org/packages/e6/69/bbf24f3b555ea1befa7db1e2510c728c8b0e80e61ce360beb7d94bc562b7/aiohttp-3.14.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f07fe3ac408d8b3f768be471dc3f56d43843c47d97c66120534467a15ead197", size = 2022390, upload-time = "2026-10-11T00:59:19.244Z" }, + { url = "https://files.pythonhosted.org/packages/5d/38/01f5fc732d5373d6c099db0343279cd4e6607287d336bab6ff6ae9085a0d/aiohttp-3.14.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f375db73a39f5cf83696d500e21a67f418dc9a988955756f254be8f03b7b3651", size = 1856329, upload-time = "2026-10-11T00:59:21.19Z" }, + { url = "https://files.pythonhosted.org/packages/15/85/1f86fbd8fe34ad447c63e679404a1d5be435fe256a9d281b791fc1ed0d35/aiohttp-3.14.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8df7d481654ac96fe1ba9a02a9f67770fdd367823e0d5ef01b922725c4bd2cfa", size = 1712763, upload-time = "2026-10-11T00:59:22.982Z" }, + { url = "https://files.pythonhosted.org/packages/39/1e/ced51cf47e427e7aa6142c3bf6f33022472703bc9061289329532d3c40c0/aiohttp-3.14.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e95c8def4b81c5d68d5cf1f54c07acd7c0d2577af244e5b6da802120825737c6", size = 1812125, upload-time = "2026-10-11T00:59:25.032Z" }, + { url = "https://files.pythonhosted.org/packages/6a/f1/9610b4e263b554ca4191501ba72b952efe06556b647da74887abc5d41049/aiohttp-3.14.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f2ebb54b3f932210503072f09974b4fb574d823e497a944adfdcd140a6a00255", size = 1829550, upload-time = "2026-10-11T00:59:27.078Z" }, + { url = "https://files.pythonhosted.org/packages/ac/8a/1d1fb27cf9fa99b13133de33044c4d10d7705be0d4173d6d56abad1b67f9/aiohttp-3.14.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:038c2c7e8caa26b6c8423779b5eaf1893904048a512c19b32fe841ffa5592b50", size = 1868179, upload-time = "2026-10-11T00:59:29.315Z" }, + { url = "https://files.pythonhosted.org/packages/24/9f/3cfafa86ff025623dcca0d30335db7b8a6f3fa348148441208147e827e04/aiohttp-3.14.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:b7806e804889231b0e06469fd4a5c06313d1c0a3377322b6d9237fa5e0fe4167", size = 1697721, upload-time = "2026-10-11T00:59:31.161Z" }, + { url = "https://files.pythonhosted.org/packages/62/1d/86439a23bf32296e715d8c75535f0cd701821d36719324baef65bcd6af9e/aiohttp-3.14.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9bab2045550c4fe0f7baf89574db1b455c195750702ba96fef1f16972b146617", size = 1887000, upload-time = "2026-10-11T00:59:33.329Z" }, + { url = "https://files.pythonhosted.org/packages/1d/0f/6044f2d02b3874966d84cd119f9b8eda46aa2991230078aea4dd17750cc9/aiohttp-3.14.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:96a2e584f0b9ed8f1fa33211397dcf67bb7069866402cb405d191c2f0defb9a3", size = 1842020, upload-time = "2026-10-11T00:59:35.384Z" }, + { url = "https://files.pythonhosted.org/packages/18/64/b49d2a34428ff937631b07dde74d5879a7a2c8559aab39331d40769e8cb6/aiohttp-3.14.5-cp310-cp310-win32.whl", hash = "sha256:602c1e9b718a3275c580149f947e7fac65044c0a20e599553fb12e9700da9eca", size = 489282, upload-time = "2026-10-11T00:59:37.061Z" }, + { url = "https://files.pythonhosted.org/packages/a6/7c/1114e723f1c4a65597820b605a0ddcd7f3b28fdfde3ac8d520d517ca582c/aiohttp-3.14.5-cp310-cp310-win_amd64.whl", hash = "sha256:bea559ad70218d230663e4210875735076a9bfea5994cef34a55a25faeaf2544", size = 512021, upload-time = "2026-10-11T00:59:38.7Z" }, + { url = "https://files.pythonhosted.org/packages/48/97/e08c646f4bad63d77876b24591d013d7f33f02fda7c931eafe3224f38467/aiohttp-3.14.5-cp310-cp310-win_arm64.whl", hash = "sha256:dca3fa8d8a0a26679862eccb0b1a9151b2b9f1cd2c212e7a6335778faaff5833", size = 493016, upload-time = "2026-10-11T00:59:40.525Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f3/8997f18890a92c79f77fcfbb4f78ca17c2d4ab109e9eb6d31b4e9de194a0/aiohttp-3.14.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d51db97c96384fbfcaf8f4c65922183a68b94f891c3c10c862ef5f6df2adbb1f", size = 832358, upload-time = "2026-10-11T00:59:42.195Z" }, + { url = "https://files.pythonhosted.org/packages/8e/42/084651e9efb5cadb99265f786df60f2a7b353cead7bfd2c87003cb4867ad/aiohttp-3.14.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ae53924aa853a7a2ca20ed4142c7c6b56338e4d4cd999e2980075b9efc2e257a", size = 559636, upload-time = "2026-10-11T00:59:43.677Z" }, + { url = "https://files.pythonhosted.org/packages/3d/fe/92838944e601f0fe195fd3f3ada37e29fbee3d19bfd323fe15937ec79d36/aiohttp-3.14.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2c473a355f9239efcb72c92d5abfd8fcdb0cc78c8e9af607e72ca12dbb36593", size = 552588, upload-time = "2026-10-11T00:59:45.592Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b8/dd9b95c5c20ceae1b47738e656bd79e9883e3117a9cc8b143901eb604e9a/aiohttp-3.14.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e8fa6644e541fcd7e02430588c7fc93b602c1778ea0bc345505db76b61cfb4", size = 1928224, upload-time = "2026-10-11T00:59:47.294Z" }, + { url = "https://files.pythonhosted.org/packages/c6/47/70010cd2ba8746968d53d433ff328c33f06a66c99da5f3de759c64a6b2eb/aiohttp-3.14.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:579f97d5120f2971876d2ddca2968135f6944d00c44c3a6590ad7d86ca9b403f", size = 1877674, upload-time = "2026-10-11T00:59:49.472Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a1/b026f071dbd87f0bdba86b92d47e46f3899e3ad7143b6e0fe7e7887090b4/aiohttp-3.14.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:24409db442e2fb6e766bc7f3943851a8381dec3098140e43bb2e843b79e31b12", size = 1985850, upload-time = "2026-10-11T00:59:51.296Z" }, + { url = "https://files.pythonhosted.org/packages/36/d1/f7b6f6f8c3cb5a71b53baeddc2e70c224c28b12a23a55c42994c504019b7/aiohttp-3.14.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5e8f97c0488ffda3082766ac0f2c8150a9a58c4d05788330e479cfd449b37939", size = 2105471, upload-time = "2026-10-11T00:59:53.191Z" }, + { url = "https://files.pythonhosted.org/packages/42/74/2a2b22953de15c6c8a80debf26db3047e4e8a5a5db6dd7c2c6c01eec0605/aiohttp-3.14.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50a195903119008fe9cc68710535eb37f556ffffd6a7759afe70a2c145587045", size = 1929915, upload-time = "2026-10-11T00:59:55.609Z" }, + { url = "https://files.pythonhosted.org/packages/33/ad/f80e8d33933d0eacd0217efa3b7fb32c9f48ed480ed0db53cf9948bf474f/aiohttp-3.14.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0133c3c3b54a0bf1e71fa5c1ad95c93f07fd54e24ef1fe182f5122e1573d2bf1", size = 1773832, upload-time = "2026-10-11T00:59:57.611Z" }, + { url = "https://files.pythonhosted.org/packages/04/a4/0273d239f3e3bb69438f209c64c82e1f98dc60d5db264795e41f7dd05dcb/aiohttp-3.14.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c172db893e516e1358e65a95ee20b7ce7173963eefe318b6ab2a2220688b999e", size = 1901001, upload-time = "2026-10-11T01:00:00.605Z" }, + { url = "https://files.pythonhosted.org/packages/16/ed/7dd439d26c654645bc34ca3c9a822fc11c1c51801fed1cefdfe1cc41c1f6/aiohttp-3.14.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f2a7966bda23dd85051f1661ce0ace38d6890e05ec6c357ecae9d2479cba377e", size = 1907218, upload-time = "2026-10-11T01:00:02.699Z" }, + { url = "https://files.pythonhosted.org/packages/32/dd/86249c3b8248562a17fd3e22ee0c164378d65766f01aceef62cf52783710/aiohttp-3.14.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:4887d130a7bbfed3a85493bb5a25e5b5b558d40c1d986dd16970d2bb26d63793", size = 1958650, upload-time = "2026-10-11T01:00:04.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/0b/ca4d53d68f683ce195807fd0aeb063bf6d1c6b39343fff234c9524e8d5a9/aiohttp-3.14.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:225c579c23b68b343cccea27a7e06e3bd8ec23a09c30b427eb3f1e4ca6239b20", size = 1760057, upload-time = "2026-10-11T01:00:06.978Z" }, + { url = "https://files.pythonhosted.org/packages/6b/42/005437ffd7fa56c2ce3347add40404b654a32754ce91f2e2a14b9e5ab2bc/aiohttp-3.14.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ab52d8f1fc1b64821c1fbad64a647ed6203627004059a6d1ed4f0858a1499703", size = 1978081, upload-time = "2026-10-11T01:00:08.868Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/3a5b66fe1b7b23d3c6524350817ed55de5feb7fbb4cccba7cbe044d118dd/aiohttp-3.14.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb131d775a1573c1aee66656bd78b023577bbdb6cb8349a07773bd4f73e68a6e", size = 1922954, upload-time = "2026-10-11T01:00:11.035Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/d08a554f281d42fd7c6b2b6dd7738f3e3a7a9f657ddd87e11ca2091cdfe3/aiohttp-3.14.5-cp311-cp311-win32.whl", hash = "sha256:e87046c8ff77a8decdb6a41d8ab25824b47531b2da933aeab0c1e21c7acff329", size = 488101, upload-time = "2026-10-11T01:00:12.933Z" }, + { url = "https://files.pythonhosted.org/packages/0f/15/52b5e65f02b33686ae49f1518548ae4f5a7adaf31d9d3c54fa12a143137b/aiohttp-3.14.5-cp311-cp311-win_amd64.whl", hash = "sha256:6f275c11d1aa6d4c458e05a68be084efe3c55a113d99e3f46a318098e52948fc", size = 512575, upload-time = "2026-10-11T01:00:14.743Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b9/bb75012635f3defb0f7cbb7c4ae391e36bb9cb3fbdca3b9944e2ebebf743/aiohttp-3.14.5-cp311-cp311-win_arm64.whl", hash = "sha256:b032a0023eb41d768ce77d83210ab2a3c389bc0b09313273c7e1eca48c10a755", size = 493179, upload-time = "2026-10-11T01:00:16.961Z" }, + { url = "https://files.pythonhosted.org/packages/d5/94/6ba86efddcb616c811b40e6a0dfdd862738f647e4e3860a961075d5e9ed8/aiohttp-3.14.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df37b620684e19b5e25724412518ccafc3b1a49cdac706fdbd2f983fad943450", size = 817499, upload-time = "2026-10-11T01:00:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e1/7bca6d84dabd228aa8eb4b7f9feac2586aaa9be5505d7d65bf287a615c43/aiohttp-3.14.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef60869969180ec2464f1349aff07138ae35ca2200f0946cb3552e49e8f301a8", size = 549467, upload-time = "2026-10-11T01:00:20.854Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/11b89f45ca486252dd67dd5f3231fec04bf5518da39a95cb3997619f17fb/aiohttp-3.14.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d079c0a0135c36e7beb6f1c88087c8f108dc5891cdd0b5eafa778421bda70ed2", size = 548067, upload-time = "2026-10-11T01:00:22.659Z" }, + { url = "https://files.pythonhosted.org/packages/22/ff/c6615806c14aab34f82b9424ccde8ce6e417315fd57ce1c5b4d4747888e1/aiohttp-3.14.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abfda5cb094a829f7bc25216a32f7db2e85cc65bd59910f8e7b40b3d9b224764", size = 1896245, upload-time = "2026-10-11T01:00:24.638Z" }, + { url = "https://files.pythonhosted.org/packages/39/b2/25a8c971ae6a92c8394d77e42422d7f38989e05cf41a5ceb92d73d67ab7e/aiohttp-3.14.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9cc882cf8619109583c906b4d4a85d6a111a98afa34b7a450d1e08118d016820", size = 1872180, upload-time = "2026-10-11T01:00:26.838Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d5/99f93ea36cc5205e47c1e5a803e087f2ad21b5430b5db2e942cb6e988a37/aiohttp-3.14.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7457580535e019e1247ea35d6a02bf081ad30c26d0cbc210c93f6c3ab67a0835", size = 1937261, upload-time = "2026-10-11T01:00:28.74Z" }, + { url = "https://files.pythonhosted.org/packages/40/a6/9ac9c9e6695040bd73d2584a1b59a9388f6433c76d5294a7bf591e21ffe5/aiohttp-3.14.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c5ed596aedb9c42afd3fe0aae3117725378ac73d2cc5ddc735056fbdb96c5d02", size = 2064994, upload-time = "2026-10-11T01:00:30.623Z" }, + { url = "https://files.pythonhosted.org/packages/da/e4/aa172eb534b7f02f1f8ff1c3213347eaf3cf218db91a727c6863c22f1035/aiohttp-3.14.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20f085697d7e911f1f73c43ed03fafbed1e7121797e2eb5428efa80398060584", size = 1919630, upload-time = "2026-10-11T01:00:32.548Z" }, + { url = "https://files.pythonhosted.org/packages/06/7d/4eedafc5bababa8932636c141e346806966eade12c0b7e5946d43bf8218b/aiohttp-3.14.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:74b0a9c8270f9b0a11410e124ff8d4f18bfc1f1837440ec84da5ae7b50927b5d", size = 1710016, upload-time = "2026-10-11T01:00:34.471Z" }, + { url = "https://files.pythonhosted.org/packages/b2/94/eee018537ba19da0ceb2ac79cab83faed4ac49568e08376e2799043f2538/aiohttp-3.14.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:19e2ba471507c34f8252402ab50f5ab512398b9ea8c8f1cb26beb3f75793ba30", size = 1858112, upload-time = "2026-10-11T01:00:36.581Z" }, + { url = "https://files.pythonhosted.org/packages/68/76/354653a306547238f3427283905972d796ba7c292ba9977abec9f2b6f260/aiohttp-3.14.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d418ce2af40c6bb685b3f663e9e8de27cb0a22431d8e88a167348d7f01878073", size = 1896371, upload-time = "2026-10-11T01:00:38.478Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ec/63e8c7136b570e356345ad3174e3820fdc973ea10712cb6c649bf875755a/aiohttp-3.14.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:70cb4008ac2ed1e0ca9e824deb4b53d3aa0d939109698ebf1e723a84337bd794", size = 1905836, upload-time = "2026-10-11T01:00:40.527Z" }, + { url = "https://files.pythonhosted.org/packages/57/d8/11365bda144b127928cd42533d0eff78a55613c9e28f81941bd6630ea887/aiohttp-3.14.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:a23fe35d776bc03cb495938b9594450d047e3bc08c5255315a82323e9cb7d2dd", size = 1696534, upload-time = "2026-10-11T01:00:42.686Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3d/82df0461b18e00b2998f205c03e0d3010222478c43640aceb8e03dcd7e8f/aiohttp-3.14.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3e0eb43bed3c6801a6cee315195377789e90b2a72c2277a475b578535312488d", size = 1933970, upload-time = "2026-10-11T01:00:44.71Z" }, + { url = "https://files.pythonhosted.org/packages/03/ad/6ddfe0aacd931c17b53533336d97e9d11a98b96d6ae815a9da0b19f82ccf/aiohttp-3.14.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be7dd397d64ca3e1869626fa9318aaebb54b7bf93bc72d7a205448d83e4f748", size = 1899501, upload-time = "2026-10-11T01:00:46.629Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8e/189bdd9ae4793059bb09f6dc880f211a6c6c7859cebcbf33912dbfab7dd7/aiohttp-3.14.5-cp312-cp312-win32.whl", hash = "sha256:eb324e2009fb54db30a071dad7caf6998ee2879c4704007efb244514dad1fec1", size = 480370, upload-time = "2026-10-11T01:00:48.468Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ce/1f08114679d49655b30a6e0a29858375c94b82c1de1a0bd0a20c2fee8b02/aiohttp-3.14.5-cp312-cp312-win_amd64.whl", hash = "sha256:2cc38a4f2b516bef1714e690df87a0e043faf1a7693c82d860091684453d5111", size = 506141, upload-time = "2026-10-11T01:00:50.272Z" }, + { url = "https://files.pythonhosted.org/packages/79/d4/c7b4f60b16a1b7e43249fa9031ae05e7e8c341ba4b7d1866f914dafeaa0e/aiohttp-3.14.5-cp312-cp312-win_arm64.whl", hash = "sha256:a63afd1f757de949028387e65a7127b61ad0f775432dbb0e62816ae619fe69ac", size = 488108, upload-time = "2026-10-11T01:00:52.321Z" }, + { url = "https://files.pythonhosted.org/packages/d3/e1/2841e020ebb7aefae5513586193e011e06313d9a6bdbd296622afbbce204/aiohttp-3.14.5-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:9ad7e6aa38c20da1be697874349c4c273c8a03b7887169665081706398d0439a", size = 538932, upload-time = "2026-10-11T01:00:54.3Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a6/7fb8ea8fe96bcc7b7c7a36d10f021d99d01a8dc8a4b3f0ddacecfad9a80e/aiohttp-3.14.5-cp313-cp313-android_24_x86_64.whl", hash = "sha256:f59c7673465908cbe506117176156c127f29f917677afceada34957179221d91", size = 553329, upload-time = "2026-10-11T01:00:56.442Z" }, + { url = "https://files.pythonhosted.org/packages/de/64/d056e3c27647dc25af1a592cf356245382ea7c808171b9dac7677afedfc8/aiohttp-3.14.5-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1b5416552740edf07234cc9437d0706f2acb67b93c198670b1a68e1b2b587dec", size = 521367, upload-time = "2026-10-11T01:00:58.345Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e4/95226147e11d4db916fd1d495dcf85af8e3816e38333e42718241196e848/aiohttp-3.14.5-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:43351bdb5e4c3cb7d1772368e988534e869a74db7778079a83782c11c69535c7", size = 528322, upload-time = "2026-10-11T01:01:00.211Z" }, + { url = "https://files.pythonhosted.org/packages/17/cd/1d3c9192cafdb51cad62b2d3ded96cff9cc8af51893210aadd448a325389/aiohttp-3.14.5-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:c8c4478bef6d57fcfda15dae461ea3c9f06aa7b257c58df3f2300174ccbb185a", size = 539874, upload-time = "2026-10-11T01:01:02.06Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0c/dfa33aecc7d4d1dc75e05248f5eac5a0edf4d09e7b44d93ab62529b0c1db/aiohttp-3.14.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c2c30484dd1417ef98b51021ffa2cc0d7f3c78918adaaaab7e70817335ab3e02", size = 813836, upload-time = "2026-10-11T01:01:04.01Z" }, + { url = "https://files.pythonhosted.org/packages/33/17/4a63738052d20567d55529d6daa1b9480d906fd52930fbcf6d3fbed618f0/aiohttp-3.14.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dab9ac5a67c8d1f070c00fa8fccb7cbd1b8dcc1a8d6b42f37540df9b3d4cc603", size = 548070, upload-time = "2026-10-11T01:01:06.035Z" }, + { url = "https://files.pythonhosted.org/packages/15/e5/b57e58695a757fd4c02497c033fced96a69c631b13866c43d336530c9670/aiohttp-3.14.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e1cc2bfaee8c214f06080a7c7d5772419b8a1108e8e5349236189811823fb02a", size = 545786, upload-time = "2026-10-11T01:01:07.817Z" }, + { url = "https://files.pythonhosted.org/packages/9a/68/8c2c67a3aedf46e00f3c42f04fbc6983de80d4ed5786151e33681ba45883/aiohttp-3.14.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74efb69332b85675b1eabd760a8cfc2e2cf42c60607c66f88014c1bdfb40942d", size = 1886492, upload-time = "2026-10-11T01:01:09.834Z" }, + { url = "https://files.pythonhosted.org/packages/ab/4b/74aab5e8d28c62e8f795b4fe8f38cf5586fd264a9a27bd2141ef6490333d/aiohttp-3.14.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:42b5e616946dbaf505e2bff18c9af2cd4ef9e7ef300ee58a6e951a5b7cf147ae", size = 1870057, upload-time = "2026-10-11T01:01:12.045Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f7/eafc3b1988302b1815d9fd4a21071be5c360d616c0a430d02fd92dc97688/aiohttp-3.14.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f9033b43f511f27547c557dcaba0177649e10a3725336ccd2cce0fdc1dc4850d", size = 1925119, upload-time = "2026-10-11T01:01:14.09Z" }, + { url = "https://files.pythonhosted.org/packages/a1/04/78d8f294f74dd570f3898ff20402349fce524176045df98ba727d6846a68/aiohttp-3.14.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15a310d3c71398e3d7bfc93a1a73fbe664315cd9e9016b8efc1cff85eeab7155", size = 2053137, upload-time = "2026-10-11T01:01:16.344Z" }, + { url = "https://files.pythonhosted.org/packages/32/51/395d225ef36f5a50d8e548dcd3141bfdbcd31fb6eed859022c573d2c4d66/aiohttp-3.14.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ffa3a523a36d8628f98c06492ae16a31a23d14c0b4ec721757b477319f656d6", size = 1911038, upload-time = "2026-10-11T01:01:18.653Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a4/2aec1aa06d82e8a244843b5dae31d78061e5e76744270a86dd0ee051c889/aiohttp-3.14.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5fb6a6e919bfb703227bc1ce6579281b84b1a2ba57deb9794dfdbec7dcd1e40c", size = 1702205, upload-time = "2026-10-11T01:01:20.904Z" }, + { url = "https://files.pythonhosted.org/packages/16/27/6051bfde7b6f418f70edd60d655fa426abb3355fa0981764739d87ecf160/aiohttp-3.14.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:43e1b7994a8b038125f722bff07492ef501110722c2727c408995d9fb864c421", size = 1848588, upload-time = "2026-10-11T01:01:22.918Z" }, + { url = "https://files.pythonhosted.org/packages/9e/44/55efc06fc26c4e6e1c095f231b4c222bf2d86d64a8eebbc24b2bb5958ea8/aiohttp-3.14.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5f3e96071686755d9cd3600c3880183eb94b012178e92746d68101800f0ed8a3", size = 1887540, upload-time = "2026-10-11T01:01:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/48/dc/1502bfdc2a65760d386ac6a00090b0addaa8a3c9c60b3f8127fad3a9afb2/aiohttp-3.14.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:cd88b01f3d37b7a2a34f91d98f14720206f1ea3d540843fab2d649dd5fb91fec", size = 1895030, upload-time = "2026-10-11T01:01:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/93/7e/44174bb6288264418c9eec07a5e35180969c0d5a796c7af544db3cb8a33a/aiohttp-3.14.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f2ed8b64dc0c651c0f5a9c926777719770021251b8f336d97c4b80b660836ce1", size = 1693244, upload-time = "2026-10-11T01:01:29.39Z" }, + { url = "https://files.pythonhosted.org/packages/47/dd/b507d64e50db23888582fff08eda13998b12f9dea70c072edaac19218380/aiohttp-3.14.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a9918e58faf62ba2c7147927d06057aec78f42475aff5048047ec47e7265a600", size = 1926308, upload-time = "2026-10-11T01:01:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/5b51b4f63e3f2793151f4aea49c48fe1e00baeb7cec9c7a206de499f8de0/aiohttp-3.14.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:42f320d4a5b00b9af0bddcfec5407dc6f2d9816f006b2f79ebbaa31f16895df3", size = 1888406, upload-time = "2026-10-11T01:01:33.715Z" }, + { url = "https://files.pythonhosted.org/packages/ff/13/d5e818a5eaba9f822016727299f41c0e1075799f82a6433ec508a93ab867/aiohttp-3.14.5-cp313-cp313-win32.whl", hash = "sha256:3ae800a20947e2c2e53088047d021e6bf7d51560cc49f6a0737a1f79d2e3a13c", size = 479970, upload-time = "2026-10-11T01:01:35.677Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d0/8eca2c65aa467320990d78fb2005f38ed3588944280c39ff9deb6423fef1/aiohttp-3.14.5-cp313-cp313-win_amd64.whl", hash = "sha256:d05e94cdfe0d15d0206f970722d2554780ce562787b21b218b275447f8751319", size = 505985, upload-time = "2026-10-11T01:01:37.574Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f8/4cdd65305d2fca14b886bea9ed2abb1fe726287872524e56e3d26692b47d/aiohttp-3.14.5-cp313-cp313-win_arm64.whl", hash = "sha256:f001b571ead90ca1770f1e616db255351a1703317f20374c361ef22f12c06d09", size = 487629, upload-time = "2026-10-11T01:01:39.477Z" }, + { url = "https://files.pythonhosted.org/packages/43/be/3184a1d34a8be665569eadb7e9e764b4629e4f3413e241cb2e4d6fecf3b3/aiohttp-3.14.5-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:939042d5cda21d41a6f512e7cc8b8e33a2aebff863352251da495fbd91b673b5", size = 540673, upload-time = "2026-10-11T01:01:41.354Z" }, + { url = "https://files.pythonhosted.org/packages/60/2a/d35f3ba4cf157b072e3b674bf9983047ca5ea5173c995d32d877e1191d36/aiohttp-3.14.5-cp314-cp314-android_24_x86_64.whl", hash = "sha256:6da32b5ff3fd78d244e37300463434c7145162bfd2b6e9e915ab164da37f7343", size = 554087, upload-time = "2026-10-11T01:01:43.719Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d2/61a33880ca4eaca95a9c60ca3f6beed15555af1028652dfaac601627787b/aiohttp-3.14.5-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1b438b73c38111818d0c9d6a5c2bfed8584c8e503a49ef085d70e874ec846738", size = 522613, upload-time = "2026-10-11T01:01:46.154Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/de579b299d2225dc2c6fd99d579d91f16c02a913fb5af9a3cf2fe9bd88ba/aiohttp-3.14.5-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:755933b107ea7a6a9ac916f635a70595a5b1a32fac10a8ff0b9f2ab88555550c", size = 529682, upload-time = "2026-10-11T01:01:48.477Z" }, + { url = "https://files.pythonhosted.org/packages/f4/4a/ddb923564e15e053b6e060b0036e1694dcadcb13aa476c5a87dcad20e336/aiohttp-3.14.5-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:b3cc509327c7b27f6f4727a8830f4004f6df7766e179f2f4b8e54e65c0bec5d3", size = 540120, upload-time = "2026-10-11T01:01:50.474Z" }, + { url = "https://files.pythonhosted.org/packages/3d/36/a640fbecaa53727a5900b892bdbe17b5f3e8cc88903e22864fe41b654def/aiohttp-3.14.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:7bd8ac754ebd6733a3e2a0dd1674c4d8ab086196803fd8dcd776f07b4e2607d9", size = 818795, upload-time = "2026-10-11T01:01:52.665Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b6/d52ca608859e271b5fa7944074802dc45f60e52c318a4ddc34edbf73586e/aiohttp-3.14.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e724a7b6091f0b1ac064f9d1b15ff9ec52e6033a86cdae649e5f086e32a3c0db", size = 549667, upload-time = "2026-10-11T01:01:54.65Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b5/05b8ac39a76ff4bca89f42a4c2471560c71f894ff6e4bc16158c951874e3/aiohttp-3.14.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c32e26310cc10e547f53cd13d39a369034f69dcb7d749d5cb0e5f67bc196b6ba", size = 548922, upload-time = "2026-10-11T01:01:56.547Z" }, + { url = "https://files.pythonhosted.org/packages/19/b0/5aa186d56ce2334dabe29b70bd99dc8ae926ee44184c0de64a53d415a4f3/aiohttp-3.14.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1eb8167961ec4dfcc8cb9dd50bd0ee72519f7ef496be95203e49e27b01618382", size = 1882422, upload-time = "2026-10-11T01:01:59.258Z" }, + { url = "https://files.pythonhosted.org/packages/db/f7/7d5c91bb9620db300c8ddb05337a9014301acb626223faff3abcb8ea47d7/aiohttp-3.14.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c1d60eafd9c7e8e74abd03a5b00df44e7febfe6d9b89b559c0a6551eef0699d4", size = 1843621, upload-time = "2026-10-11T01:02:01.417Z" }, + { url = "https://files.pythonhosted.org/packages/30/0a/b208953b96d8f24b75f6da704f508e6c5cf3022f52b60c61933df082e89c/aiohttp-3.14.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:137351bf20bbed9a65e839f4a4452ac377389bdb2f2857d2acffef38f5e9f2d1", size = 1927915, upload-time = "2026-10-11T01:02:03.697Z" }, + { url = "https://files.pythonhosted.org/packages/f6/79/90ebcccb55e2d1e11a1fed581d83bb966e38fb35fb4b2577fdc980f8707a/aiohttp-3.14.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fba47bc2c3d7303c3d027c6cf4d07626c37b1314ac81f5820c31032e0ca1f677", size = 2036339, upload-time = "2026-10-11T01:02:06.046Z" }, + { url = "https://files.pythonhosted.org/packages/a6/66/55a8904b3a129fafdf94f9cc0a2e4ca09a3c914650be52355db7ad0bbdb6/aiohttp-3.14.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94684b879ac1d71e4238850c99b62dc1b28d9086b156a2555f082010b85a865c", size = 1888041, upload-time = "2026-10-11T01:02:08.384Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b8/96b25da7329a52e42c812b1e8b076386039ec4fc312afa043d173d8147fc/aiohttp-3.14.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:56572c42e3ecd636de8d2c3dd54cf5fc939cb5c32eb56297f176a0d366fac622", size = 1703849, upload-time = "2026-10-11T01:02:10.903Z" }, + { url = "https://files.pythonhosted.org/packages/1b/43/fbf976e3ae4c038d6f5c84945ab2150298c2d71201674d4e53d158063e75/aiohttp-3.14.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a95529a92a446db351675f4aab518feaf5e99842f63f5dd17160c2b74f382db3", size = 1849703, upload-time = "2026-10-11T01:02:13.15Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7d/218e912f4c1d89bde7ac551409be57ad2f6121638e56942d395a7cb1fa58/aiohttp-3.14.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:3edbece0379b8b4aaa67619b8aa2399bb66fce372cd5911098a434ea77220aa0", size = 1852695, upload-time = "2026-10-11T01:02:15.295Z" }, + { url = "https://files.pythonhosted.org/packages/5a/42/252a1b9287e3b6e393a1c3bd1776f36af30f5f25f071bbf2b0cb7eba9116/aiohttp-3.14.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:56d9828f204331a5ca8850fcfe2bcce95a149f1f223f60cc7216e5524978e480", size = 1893493, upload-time = "2026-10-11T01:02:17.93Z" }, + { url = "https://files.pythonhosted.org/packages/78/97/71cae83d5100556fad1521684f7cd1e3e578432644f850245ed3bd969310/aiohttp-3.14.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:20064a177a070d789ee64a50b01a9161d3468e989baacfc6c714aa685c4b332f", size = 1693468, upload-time = "2026-10-11T01:02:20.666Z" }, + { url = "https://files.pythonhosted.org/packages/1f/69/73d88e97a8b5f0ca7a946d0011c0de99fb188b1687c7948ecd0553dc5bf0/aiohttp-3.14.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:81c2b3dfd56c62bee6108e4852d5970b4cf9086390b6983f52b666e878c1f115", size = 1915398, upload-time = "2026-10-11T01:02:23.011Z" }, + { url = "https://files.pythonhosted.org/packages/05/f0/881644bcb15d4b258daea9b720a0af9dc4330496cc8d6ade9090cdd0cffc/aiohttp-3.14.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:09ec102b4b8c9a920275733bbc11fdbb615efe6f9231a06007c0218d336fb77a", size = 1871470, upload-time = "2026-10-11T01:02:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/7f/de/19d9ebbcce5aedaa3242d8a99ff8816a60bdb7629fb0084bf4a45bfd1f62/aiohttp-3.14.5-cp314-cp314-win32.whl", hash = "sha256:9c428eb2bd8817588d16a0ab898aa4eb5d141f896aa2b394cc79a4cf61d9a8e2", size = 485517, upload-time = "2026-10-11T01:02:27.579Z" }, + { url = "https://files.pythonhosted.org/packages/a9/74/8cdaf0e58c2588371670d5a9a8215bbb971d36940b6e5d051967dd05c07d/aiohttp-3.14.5-cp314-cp314-win_amd64.whl", hash = "sha256:6f967dde489ca6a8c02d093ab245d2cbf50ccb5c36adf0188b17b0ca39d24b67", size = 511755, upload-time = "2026-10-11T01:02:29.685Z" }, + { url = "https://files.pythonhosted.org/packages/1a/6b/e0100e25502430a531c7cf1482a378d0b65bf728ab60c01ee270e56bc469/aiohttp-3.14.5-cp314-cp314-win_arm64.whl", hash = "sha256:1d2d981b53dd09a319e3570ef8cc3bbc3ef86f5a7abef0f6b2bff3867db3a9e7", size = 495151, upload-time = "2026-10-11T01:02:32.163Z" }, + { url = "https://files.pythonhosted.org/packages/9d/c2/ca2ead7b655688c53c03aeeb6e96e6851c9ff08d6be7b13802f53a6ae8fd/aiohttp-3.14.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9ce66feae6ac65327379460380549bf1b8df8e17c4e25df2a2bcf168272e3bed", size = 855396, upload-time = "2026-10-11T01:02:34.443Z" }, + { url = "https://files.pythonhosted.org/packages/a7/70/22206fea409255a240c926ce11de48de354ae2bb90ca44f709c05497585d/aiohttp-3.14.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:27c2322e03f66101acb09869ce1cf1efc04994ee95e1735b69827bf8c8b9d781", size = 567734, upload-time = "2026-10-11T01:02:36.644Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e5/79a36c118308b56f8667d67e05d2fb6dc638ab45985704cdb199631bedaa/aiohttp-3.14.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff75a7537413a86e7cafe98e0e1d6e3dc4b15c6349896e7d5c6b881bfdb6d550", size = 567379, upload-time = "2026-10-11T01:02:38.757Z" }, + { url = "https://files.pythonhosted.org/packages/63/a3/2ebec7dece3b1f02c30d2e484647f6f7b13952b1bb40a4cb285b849e8432/aiohttp-3.14.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c061aa954daaf57d2a4b8374f9fca621ef0e1b603584431c220c22458c59b6d", size = 2050686, upload-time = "2026-10-11T01:02:41.169Z" }, + { url = "https://files.pythonhosted.org/packages/3a/d2/7e4d093db2f4450482652e7ef19a9e19919028f5135aa52bc4078c3beb80/aiohttp-3.14.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1612fa5857b37bf32e5c1eaeefb96e3b01e9c70679eec81f0934e8a600080863", size = 1915077, upload-time = "2026-10-11T01:02:43.563Z" }, + { url = "https://files.pythonhosted.org/packages/a6/88/bd40d09958442a0a1df67da81de496361d6e2afc04f9db2950d835da750b/aiohttp-3.14.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:adbeee7d6fd4cf5fe0aece2fb3edc4243615d3180430ba8149d01a90670cac99", size = 2027204, upload-time = "2026-10-11T01:02:46.185Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/3a601c1f8d3103c1a60ea981f20924855da9f575d2007fc38f8898b792fb/aiohttp-3.14.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b2966998927d7bed9db12c0a4647b0c7b179755878fc9c357fe1ffd3e3b0c1a5", size = 2149520, upload-time = "2026-10-11T01:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/22/d0/4e41bfe1b1ce1cb6f6d2e59fa7a88ef5cf92c402b2d07e9018778ba9edbc/aiohttp-3.14.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e317e0fb6b16212c881d2205a7d87414c29acd69320b3aa6dce9d9c7b86fe4f", size = 2002039, upload-time = "2026-10-11T01:02:51.293Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/72067019545c502b881b031153c437752ecef48d7d213bc0218ccebb4bfb/aiohttp-3.14.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50343c1757b4b6f6708eeaf24534b32f19dfb99fb1b762c00420867a62fc81e0", size = 1786164, upload-time = "2026-10-11T01:02:53.533Z" }, + { url = "https://files.pythonhosted.org/packages/d9/fe/7741efd6119bfb7a00827fe6f7b84b4409de58d888ae21adc5a9a6824992/aiohttp-3.14.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:083673c7a94c3ea035caaa5ca04288bdb44887abfe1f5ba23294e6a4b03efd2d", size = 1970689, upload-time = "2026-10-11T01:02:55.888Z" }, + { url = "https://files.pythonhosted.org/packages/43/e7/342a13bf67f34d269bf2f7e870ecd72b99c832cc6f0a271a9210c0ebfb84/aiohttp-3.14.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:2528cb4c6b92008c76ac9ac6298624069bb2db91ff4929905512d1d84485f658", size = 1951967, upload-time = "2026-10-11T01:02:58.467Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d4/fdb3b27340617e5e64df18a70fa89097778652ea5c7c7e2f79def76999c3/aiohttp-3.14.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b1b8ece1e71132d2afba4dbc0c3d62c766e25165990b25db1196c04969eb3d84", size = 1968511, upload-time = "2026-10-11T01:03:00.883Z" }, + { url = "https://files.pythonhosted.org/packages/83/b2/e8f88298de78d1a951f36f9f966d38ec6ed1d4721303ba02064a545e8aa6/aiohttp-3.14.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fce9523df31cea6284f3e2c479876750d7687cf671d7b25d32b19effc0e86441", size = 1778497, upload-time = "2026-10-11T01:03:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/22/68/9ccdb93d664345c546be7f34480b921774d8c0d98f47e70d7e03b115d475/aiohttp-3.14.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:09e0eb18c7e0c8777e2f9149de63799195b9b3ca1b5c81ba6f32f2c6b8628210", size = 1999676, upload-time = "2026-10-11T01:03:05.829Z" }, + { url = "https://files.pythonhosted.org/packages/57/4a/a33cfa6dcb00e94194ae4fe710432ca4ed111e016356b4ba4d2f4c3a124c/aiohttp-3.14.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6774814fd5c338e72ee0da5cbb9432816df450e69c019f72b5d29bdec2a1792d", size = 1963437, upload-time = "2026-10-11T01:03:08.196Z" }, + { url = "https://files.pythonhosted.org/packages/f4/20/eacbecfea3b5c3dcbfc9b023e3a5460f43e5dda16d06a2877ebe7184c3f3/aiohttp-3.14.5-cp314-cp314t-win32.whl", hash = "sha256:33f706574e32c6e694f352a856e05caf18f7f2c871b3e87b41c55ea452b409ab", size = 503973, upload-time = "2026-10-11T01:03:10.481Z" }, + { url = "https://files.pythonhosted.org/packages/ff/78/18eec294f6c8c5dc845dcf6d730a0147d8d0f17e86138a7bdb85e43a30fa/aiohttp-3.14.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5ba14a839fbe87cf7c12a6b5661c05f324a296eb8363141edb3944ba63d4c9d3", size = 531190, upload-time = "2026-10-11T01:03:12.716Z" }, + { url = "https://files.pythonhosted.org/packages/9d/39/e53f8169acc85271ebd12b5b32ad7f1541b35639ccbe0f49034c64785d10/aiohttp-3.14.5-cp314-cp314t-win_arm64.whl", hash = "sha256:1061b364556e8172e8d46b0b183adeeb73e8c42d30ebc745591e1bd89acad52e", size = 510360, upload-time = "2026-10-11T01:03:15.08Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1c/06d89f58b2db3ee92dd377217659d587e06f973e57bf0a97a0d8a4586c0c/aiohttp-3.14.5-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:788ecaa9c10533b786ce5ba70c4f2df78ad41819fd00a6c99d92b66f9a32e1da", size = 540674, upload-time = "2026-10-11T01:03:17.483Z" }, + { url = "https://files.pythonhosted.org/packages/18/39/5e822e038f496f0540ada91e27099d48f9e7f919b6c6deb4cf6db36cc706/aiohttp-3.14.5-cp315-cp315-android_24_x86_64.whl", hash = "sha256:5c76f1802bab718a68ac3cce447160605c734551f95c67ae90fa1132b215cb29", size = 554183, upload-time = "2026-10-11T01:03:19.71Z" }, + { url = "https://files.pythonhosted.org/packages/9b/ff/0cf2619d902b5b160762422a7e5e02091295766a7fe4fcf5b9a655e386c1/aiohttp-3.14.5-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:a6d02b4c38de03d9c7617813433e6a0fb6b522797974177d69d9dad431900833", size = 523160, upload-time = "2026-10-11T01:03:22.193Z" }, + { url = "https://files.pythonhosted.org/packages/86/99/3553abfc53a40849dbaacc3f54c730ec58410809ffa2d05885ae56108f2d/aiohttp-3.14.5-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:20726f9782d5c2744c1c66255842d1d163bb3edcf768b8de25216bf47f7b6ccf", size = 530311, upload-time = "2026-10-11T01:03:24.435Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a4/5d25f73754bc1e8f983ba704e86d641aea290c967f195f2aeebdaed2bd84/aiohttp-3.14.5-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:248d779ad720b49d4fb355720e60c9e5f444f95887bc16974fea48fc56c41789", size = 540274, upload-time = "2026-10-11T01:03:26.698Z" }, + { url = "https://files.pythonhosted.org/packages/29/a4/07eda5db2e3ee017d9590f36c12a94ec6f1f50516e8df78672373dfc7185/aiohttp-3.14.5-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:0a8ea271867e360ac985ae607f4a23ad9a38414b9aca1d49ec98839ae660e49f", size = 819239, upload-time = "2026-10-11T01:03:29.481Z" }, + { url = "https://files.pythonhosted.org/packages/cb/aa/a8723dd987a696dd48d4cf2f0088e589ce77caebff0b96f2a78c20424380/aiohttp-3.14.5-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:823c910f046f23f4c713b8d99a2242dc65f591cb45ee86418fa11762a3c2963c", size = 549832, upload-time = "2026-10-11T01:03:32.02Z" }, + { url = "https://files.pythonhosted.org/packages/29/5c/969a1b72692055fd2a419590c41847ec9144fefb97b75ff1cde5b6372891/aiohttp-3.14.5-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:9b42db919715e91eb76acf3bc492a9a7ccd8bd9adc6745c1412b689735269f14", size = 549234, upload-time = "2026-10-11T01:03:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/38/05/8e3e07fd8a0d33d06955ff4e54a1cb92f4bce347ff55e441dc3e25a7b5e5/aiohttp-3.14.5-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3112585250b199296c26ca6e0131640b6a8d01bab8b232d2eb3763ed469de11", size = 1891314, upload-time = "2026-10-11T01:03:36.899Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b3/05a79ce2e25f024e93de30c94f39dc6aa6e2bc1e9c531a5c4b18dc61b7a4/aiohttp-3.14.5-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c147451b4a58e7050f7f7394e6c467867c84161560001f9ad4fb2d1446743946", size = 1884372, upload-time = "2026-10-11T01:03:39.334Z" }, + { url = "https://files.pythonhosted.org/packages/94/52/0fd8af0717db109eea258191b326b5cb5847fb88928bfa6c862fd78b9ad3/aiohttp-3.14.5-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bf163cc701f3d4ac43ba7d97771bf5fd955220ef5500ef3ee847bc0ecfbf4ec1", size = 1930957, upload-time = "2026-10-11T01:03:42.172Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b3/fa78733da88812bf9fb193913fb0ce1548f8b6912047633fdf88a758ff8c/aiohttp-3.14.5-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8d40ce41991e9d56fab4f5dc4a51fe59bc3b5c77c27f4b148963064d00232e8", size = 2038886, upload-time = "2026-10-11T01:03:44.646Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f5/2fcc5e30053a938286f17d0edf3f0850b8061b984256fa7c26850b9c8809/aiohttp-3.14.5-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:276a4fc00b1d9ae492b802763a789c5b86328b989c5ea169f2faa447d6a11c7c", size = 1894470, upload-time = "2026-10-11T01:03:47.304Z" }, + { url = "https://files.pythonhosted.org/packages/62/2a/f87feb42abe8e6a7c03814dbcb711540849a1e90aa392055f3183c643610/aiohttp-3.14.5-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:50983e3be33d8c0942ab88cec3905b10602f64c469b20153c48c5d4e558dd016", size = 1716273, upload-time = "2026-10-11T01:03:50.121Z" }, + { url = "https://files.pythonhosted.org/packages/81/b2/adf1f960dd977722ed1347d33da512a1624807114f57a3b91f5cc828e081/aiohttp-3.14.5-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:16c8abd5bca220a47efe667d26f8460124c81810787e79ee87b242677563d9dd", size = 1857513, upload-time = "2026-10-11T01:03:52.959Z" }, + { url = "https://files.pythonhosted.org/packages/d7/fd/ef8d910e641de4160026a513ace5888b7f92826bbc3fd90ced05d55a828e/aiohttp-3.14.5-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:0790ec66fa4013e83c53b9025a45d454723da1a2fce28b3208c9b32d08af162f", size = 1887704, upload-time = "2026-10-11T01:03:55.641Z" }, + { url = "https://files.pythonhosted.org/packages/7e/db/6c9142f941cba8d35be8e1fae6ea2bd390e754fc14076b8147aee1a4592f/aiohttp-3.14.5-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:cb11a971a3aea10f9b8373be628f1df932964fc6c6b174516d318a48c3ac4412", size = 1895684, upload-time = "2026-10-11T01:03:58.18Z" }, + { url = "https://files.pythonhosted.org/packages/e6/7c/6a6bd9a72e576d376c668333b00c51c6147aba4fb863ed6c94996d497eca/aiohttp-3.14.5-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:932ce7e694bbc29b2bf6f64f2343c27d148d4997c771d01bdade4639b6749ff4", size = 1706299, upload-time = "2026-10-11T01:04:00.8Z" }, + { url = "https://files.pythonhosted.org/packages/69/ec/d2cc494242f8c4d3d1cd70baf591742818a74386dc3b84af3195c5c4fced/aiohttp-3.14.5-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:a7d470cf7b206e6359fc77b1b860632fde400d5a2ed59cd0181b93a686bc81ee", size = 1916645, upload-time = "2026-10-11T01:04:03.892Z" }, + { url = "https://files.pythonhosted.org/packages/a3/6e/e852c53647e815db09a1b6b5ab634f54bd736412397e11940feef4d2c89a/aiohttp-3.14.5-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:6e1d8637cf73eebc92eba2e11d4cfff98a3b562f2505bd75bba766d908926e8d", size = 1875408, upload-time = "2026-10-11T01:04:06.922Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f6/72ab6ef20c332399be593bac543d2c24a6d241e39d3540ce9f95a63e4bd2/aiohttp-3.14.5-cp315-cp315-win32.whl", hash = "sha256:fbdc5ec49f9ca3cd24955cf3520b10a4d4c901ba2572094c84274e9e7eb30534", size = 485590, upload-time = "2026-10-11T01:04:09.626Z" }, + { url = "https://files.pythonhosted.org/packages/06/eb/e9de75b8c6d2170c42c08ff303abf857ea8a6d9d9b6e99b5aba40f15e962/aiohttp-3.14.5-cp315-cp315-win_amd64.whl", hash = "sha256:a9d3983bd6ab7aa1cfd573544ae98df9b6cb6912a5185a198263e024a636861d", size = 511740, upload-time = "2026-10-11T01:04:12.277Z" }, + { url = "https://files.pythonhosted.org/packages/fc/25/455f3c2785eb0d50748cffd0abd07500815f419a9495b14610b7622d8d2d/aiohttp-3.14.5-cp315-cp315-win_arm64.whl", hash = "sha256:e29347c142cf6e99e0dff5e2995ead1d50fa3b51bf37a7c726a7ccfe5419745a", size = 495171, upload-time = "2026-10-11T01:04:14.667Z" }, + { url = "https://files.pythonhosted.org/packages/e7/6c/497f0a98782eebfcf0f02a7fbdef5428148bd426027140cbad494cf842b5/aiohttp-3.14.5-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:9bf1d5dcc15204d9ec8b8ea4c18fd66e6b80e5de1f4ecbafb3a2f2740f8039d4", size = 855638, upload-time = "2026-10-11T01:04:17.154Z" }, + { url = "https://files.pythonhosted.org/packages/9a/c5/55c0cef2572af9b1ee81608f7c0a1bf74e6c9151a73b04915d933f192335/aiohttp-3.14.5-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:14f04769cfefe4734016a856a83af36133cd17779cef9ae817f812b8ba9d6d51", size = 567455, upload-time = "2026-10-11T01:04:19.702Z" }, + { url = "https://files.pythonhosted.org/packages/4d/47/e1a0e39f4a2b881f6071225afaf94a6547001b5aefc1566734d73c685934/aiohttp-3.14.5-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:6e4251c0ba4624a68a2c11471a1ac54c3306876c21f0ae86de085cc9241c8905", size = 567836, upload-time = "2026-10-11T01:04:22.235Z" }, + { url = "https://files.pythonhosted.org/packages/c3/1d/817d85836f52b687160064a326e62e037dc42f1ad5b6b5188a70e5c134b5/aiohttp-3.14.5-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4f5cf4dc72a71c4cfa9751b4950be22f733626670230d46e7d606592aa22d59", size = 2051409, upload-time = "2026-10-11T01:04:24.821Z" }, + { url = "https://files.pythonhosted.org/packages/f7/25/e8ea6fc212a9aabce982917346ce8ecab0929c74b60232a056dd11c99da0/aiohttp-3.14.5-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:dbf53ae2601b7fd5a93c3944deea3a78d40f495226d582c35ef7a433425ce2b2", size = 1936493, upload-time = "2026-10-11T01:04:27.456Z" }, + { url = "https://files.pythonhosted.org/packages/24/33/de0517f71f1a19feea4aff78a2ec4ec2d98634129ec30ead78fce2f8f81b/aiohttp-3.14.5-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:657291433bf4dd3142f3abac495764cd47d0c7c92087751e6666c6447e65fcef", size = 2021405, upload-time = "2026-10-11T01:04:30.276Z" }, + { url = "https://files.pythonhosted.org/packages/c4/11/ddaf2e7930543e9f0cad3a1c54e1af0c1bd2fae3973d568c4263d3b010e9/aiohttp-3.14.5-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3e51a27980c3788e6e6b3325d694fdd4898087fa8a86b2763af77b39353da41e", size = 2142017, upload-time = "2026-10-11T01:04:33.022Z" }, + { url = "https://files.pythonhosted.org/packages/1d/7b/58784353c06de8adc20f426daad3d85dd86fd55331c713a3f4b638c94573/aiohttp-3.14.5-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:82c7583cd3dfdc7dcc927835b4f6c7faae7ecc1ba3ca5879321621ae2e6f8e84", size = 2002482, upload-time = "2026-10-11T01:04:35.94Z" }, + { url = "https://files.pythonhosted.org/packages/b9/f0/417d9535caa9e165ffe6a53e2347a78107e7c87dcb0e10aca315c3af0344/aiohttp-3.14.5-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6fdcd6af7e2e51d1ba1b4bea16e97b074bcb7b5dd0246a9d8201341bb28085a0", size = 1791774, upload-time = "2026-10-11T01:04:38.733Z" }, + { url = "https://files.pythonhosted.org/packages/98/01/25e49c2e8a01b9f0e19ca0a8448ad50aa2bdf96c8cd41e92bd45af044784/aiohttp-3.14.5-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:c8859a013ae0de1074660992139a1a440df3e6b219b86cf0d3f11c2692bb4fe3", size = 1970358, upload-time = "2026-10-11T01:04:41.66Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fc/c132fd3465b6c7e4ce0193154f602c3e6c46b680e4da7eb3bd0d8a40d1c7/aiohttp-3.14.5-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:8966ecac808dd5f473c9c4cefd10cd3ffda71c18a4d3493b7c7d2ae1803bf2cc", size = 1969192, upload-time = "2026-10-11T01:04:44.66Z" }, + { url = "https://files.pythonhosted.org/packages/95/4e/d58b45e7dba4eb607eca11fc0a4aa77ae0f39c11afa804635a61ce18cff4/aiohttp-3.14.5-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:3093b72c215bda16ce961a6d073f6e71d46e022962a9d5d457c5d4d421c78b57", size = 1963618, upload-time = "2026-10-11T01:04:47.505Z" }, + { url = "https://files.pythonhosted.org/packages/a2/d9/f6ac50946efb3490428ef52b56e62c1c6b7f9c6ff3ec6083535526c83e60/aiohttp-3.14.5-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:149fb56caf7acb67073126f675d0958d9c4b3125fcd3f6d4877df98aa8a97ce9", size = 1784817, upload-time = "2026-10-11T01:04:50.287Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2f/f255eb63da788cd8a452fe350869c03266ccad7d884925f6963c87808f24/aiohttp-3.14.5-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:293d3ae7c6a0ed176a42e59a1b5fde825ead65c835360f734148e96729f928d2", size = 1995956, upload-time = "2026-10-11T01:04:53.213Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9b/241aa3393eaafda0034470add1625f82a4c252901108723b283a9b0b32ca/aiohttp-3.14.5-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:3f2dcc00191fd563e9075181a14ec31d7dd63223ced7582cc70a15a499de0c79", size = 1967794, upload-time = "2026-10-11T01:04:55.958Z" }, + { url = "https://files.pythonhosted.org/packages/5f/7f/a68e689288c9e4bfcf8d0979f2b12b774bf01861985420df6150eba5448e/aiohttp-3.14.5-cp315-cp315t-win32.whl", hash = "sha256:7779cd97e61ebe583ec2f1c5616cdd038aa08a4453b1848c67842176d054948e", size = 503876, upload-time = "2026-10-11T01:04:58.978Z" }, + { url = "https://files.pythonhosted.org/packages/a4/78/49b0299da6d54de19fc6fdc6889d50233ac47d192a461624f4fc010fde83/aiohttp-3.14.5-cp315-cp315t-win_amd64.whl", hash = "sha256:0e6f16f5e49c4b8267988c05ab07760d7064cea57d077c3d068d04b0fbb992cb", size = 530957, upload-time = "2026-10-11T01:05:02.23Z" }, + { url = "https://files.pythonhosted.org/packages/21/d4/b0afc936aeb6d2f93157e3408b069ec5d7934429ae7d023de5e0953b1887/aiohttp-3.14.5-cp315-cp315t-win_arm64.whl", hash = "sha256:1aead151c3abbac6b32942e452020cb66d7efc099d253cc6c20f748e926c858b", size = 510408, upload-time = "2026-10-11T01:05:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/68/30/173960c42b05a6c59f7558e4b12a4b0d9ba376cf6aa9bde7f9e08a30ca8d/aiohttp-3.14.5-py3-none-any.whl", hash = "sha256:efc21a454892828368b11c2c780de0ff8bc991f73f6b99c6b66e56205470929b", size = 279517, upload-time = "2026-10-11T01:05:08.523Z" }, ] [[package]] @@ -525,11 +567,11 @@ wheels = [ [[package]] name = "certifi" -version = "2025.8.3" +version = "2026.10.19" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/1436c50cc46699de0230fbaacc1d1de7bfec413176099949e9d5112e88f2/certifi-2026.10.19.tar.gz", hash = "sha256:035c1e85572b141bf1c3a3eff0e33a8eb74917522f2559075ff8d2fea780a1ae", size = 137598, upload-time = "2026-10-20T02:24:16.604Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, + { url = "https://files.pythonhosted.org/packages/c4/51/29046e48f693c54e5b7412216d447a1d8362c9653148d98c02cdf65fa733/certifi-2026.10.19-py3-none-any.whl", hash = "sha256:42d35340cc5b9ba78e818ddbff0c5b1c2b779de448a340fd1b989f5fa8670e1b", size = 136464, upload-time = "2026-10-20T02:24:15.072Z" }, ] [[package]] @@ -1032,15 +1074,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, ] -[[package]] -name = "distro" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, -] - [[package]] name = "dnspython" version = "2.8.0" @@ -1635,15 +1668,15 @@ wheels = [ [[package]] name = "httpcore2" -version = "2.12.0" +version = "2.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "h11" }, { name = "truststore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" } +sdist = { url = "https://files.pythonhosted.org/packages/30/ff/b53af1b49bb2cb5fa97c92215614b173ca5c1a99ded80e2d52f15a94fa6d/httpcore2-2.14.0.tar.gz", hash = "sha256:4f57a1a8249fe6af589d935cfd456f1d13bbdcb0f0fc72af9142368fc94811d7", size = 69096, upload-time = "2026-10-18T16:29:21.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" }, + { url = "https://files.pythonhosted.org/packages/02/06/a8d6a8f90d11b76d9086242b6bb0ee4b503473bfc8be34c2d5b067275c22/httpcore2-2.14.0-py3-none-any.whl", hash = "sha256:4c4f64f4b544afeb778c42c0ffb5941b29e671157e90ed3f6dc787c4bca90cd1", size = 84291, upload-time = "2026-10-18T16:29:19.743Z" }, ] [[package]] @@ -1661,14 +1694,9 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] -[package.optional-dependencies] -http2 = [ - { name = "h2" }, -] - [[package]] name = "httpx2" -version = "2.12.0" +version = "2.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio", marker = "sys_platform != 'emscripten'" }, @@ -1678,9 +1706,9 @@ dependencies = [ { name = "truststore", marker = "sys_platform != 'emscripten'" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7f/f8/579a8b51e42e38ee32647df9f08aa25643ae788e275cc625b199829c4671/httpx2-2.12.0.tar.gz", hash = "sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf", size = 100040, upload-time = "2026-08-18T13:22:09.086Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/67/58353f1848b4ba568e1ec22ea1e0d8e039f0bca5fde7e2d1f07739fd83e7/httpx2-2.14.0.tar.gz", hash = "sha256:47ad61a3c40282cd8d0dc96311531f00f5bfc71a066c6fe3b36d8f63fb9e4f6e", size = 102036, upload-time = "2026-10-18T16:29:22.627Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/95/411ba65569158e862368917aaf56597f3e5fa3b91b0502919638465a08f3/httpx2-2.12.0-py3-none-any.whl", hash = "sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36", size = 95427, upload-time = "2026-08-18T13:22:06.834Z" }, + { url = "https://files.pythonhosted.org/packages/69/5a/469616aa8c2cbb0bb9dd67a3c18d07c01ab604cc0b236ee1c47d260c7f6e/httpx2-2.14.0-py3-none-any.whl", hash = "sha256:9d20c13a10bae1773a0be3b0f1e5beeb4bdd549bc3a2ba18efa32897c65403e4", size = 96901, upload-time = "2026-10-18T16:29:20.819Z" }, ] [package.optional-dependencies] @@ -2079,7 +2107,7 @@ wheels = [ [[package]] name = "mcp" -version = "2.0.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -2097,22 +2125,22 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/74/33/32d4dff2c95bb5d897c3ef4c83649a08996b17b58f0a326d2495d4c81179/mcp-2.0.0.tar.gz", hash = "sha256:0f440e735c13ece8bb19bc62cf0b86f4313448432fbb77d35e14034f4e050728", size = 1662284, upload-time = "2026-07-28T13:45:32.346Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/8d/e0d339616f4810e9051d4aba6887afab289ab1f81875fe908b606cdfd0e3/mcp-2.3.0.tar.gz", hash = "sha256:8b147a50441cf059dc88c684e0aeed3687f0aa0f39c6cde7b90330effd2b34d8", size = 4171955, upload-time = "2026-10-02T22:06:56.091Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/72/7d7897418912c1d12e87556630dfb7bf0eac71160e9bef8b447960804ee3/mcp-2.0.0-py3-none-any.whl", hash = "sha256:1cb4c75d2d2c7b8c1d756355e5d82a39f2822cc7f13e22a2051d7ca3592349d6", size = 349980, upload-time = "2026-07-28T13:45:28.853Z" }, + { url = "https://files.pythonhosted.org/packages/20/d7/4a2878f128df729db3ca04e71bfce26405cc2e0c9ebe19c592feeb4c61b9/mcp-2.3.0-py3-none-any.whl", hash = "sha256:dd0c44c089d16453e8ae31a3877a0054d7a2314caaa81f5e0541b9b1734b2377", size = 368664, upload-time = "2026-10-02T22:06:52.119Z" }, ] [[package]] name = "mcp-types" -version = "2.0.0" +version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/56/9b8e1c152f61f6c6b07c4b5896c88c7d0ae90bac6ee6306f852fcc5c1eb0/mcp_types-2.0.0.tar.gz", hash = "sha256:d7d939b9285c9961ae8866ba75ef85da34d12bafe276efbf4eb6a131786d8379", size = 66632, upload-time = "2026-07-28T13:45:33.804Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/2d/7c251e34207f6c51000312fc8839111ac45cfe02023f90b44e7f1051dd8e/mcp_types-2.3.0.tar.gz", hash = "sha256:d1e46549edb35ee19a94940fcee6d1addd7e589ab7ea92dda83f5d84781fc362", size = 65898, upload-time = "2026-10-02T22:06:57.863Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/4c/c78d78c3d52b0ac594ad7cc8ef5972adfe070e3597a8a4c6ce0cd39196ea/mcp_types-2.0.0-py3-none-any.whl", hash = "sha256:6b2de797ca2797f568b79529e1b25948e34de511bcc0bd82fef1039a6d1b8eb0", size = 69649, upload-time = "2026-07-28T13:45:30.713Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/32583066b155bb5d4507e7887dc88ee23118cc4de8e2a12870c49719e2a8/mcp_types-2.3.0-py3-none-any.whl", hash = "sha256:968efdbdaedfab06adae40d378a34395f1090c5921d4be3c9cde283aaf76d91d", size = 69108, upload-time = "2026-10-02T22:06:54.099Z" }, ] [[package]] @@ -2738,6 +2766,7 @@ name = "openai-agents" version = "0.24.0" source = { editable = "." } dependencies = [ + { name = "certifi" }, { name = "griffelib" }, { name = "httpx2" }, { name = "mcp" }, @@ -2810,9 +2839,6 @@ realtime = [ redis = [ { name = "redis" }, ] -runloop = [ - { name = "runloop-api-client" }, -] s3 = [ { name = "boto3" }, ] @@ -2891,6 +2917,7 @@ requires-dist = [ { name = "boto3", marker = "extra == 's3'", specifier = ">=1.34" }, { name = "cbor2", marker = "extra == 'modal'", specifier = ">=5.9.0" }, { name = "cbor2", marker = "extra == 'vercel'", specifier = ">=5.9.0" }, + { name = "certifi", specifier = ">=2026.10.19" }, { name = "cryptography", marker = "extra == 'encrypt'", specifier = ">=45.0,<51" }, { name = "dapr", marker = "extra == 'dapr'", specifier = ">=1.16.0" }, { name = "daytona", marker = "extra == 'daytona'", specifier = ">=0.155.0" }, @@ -2905,7 +2932,7 @@ requires-dist = [ { name = "h2", marker = "extra == 'modal'", specifier = ">=4.4.1" }, { name = "httpx2", specifier = ">=2.12.0" }, { name = "litellm", marker = "extra == 'litellm'", specifier = ">=1.83.0" }, - { name = "mcp", marker = "python_full_version >= '3.10'", specifier = ">=1.19.0,<3" }, + { name = "mcp", marker = "python_full_version >= '3.10'", specifier = ">=1.30.0,!=2.0.*,!=2.1.*,<3" }, { name = "modal", marker = "extra == 'modal'", specifier = "==1.4.3" }, { name = "numpy", marker = "python_full_version >= '3.10' and extra == 'voice'", specifier = ">=2.2.0,<3" }, { name = "openai", specifier = ">=3.0.0,<4" }, @@ -2920,11 +2947,10 @@ requires-dist = [ { name = "pygments", marker = "extra == 'modal'", specifier = ">=2.20.0" }, { name = "pygments", marker = "extra == 'temporal'", specifier = ">=2.20.0" }, { name = "pyjwt", specifier = ">=2.15.0" }, - { name = "pymongo", marker = "extra == 'mongodb'", specifier = ">=4.14" }, + { name = "pymongo", marker = "extra == 'mongodb'", specifier = ">=4.18.3" }, { name = "python-multipart", specifier = ">=0.0.31" }, { name = "redis", marker = "extra == 'redis'", specifier = ">=7" }, { name = "requests", specifier = ">=2.0,<3" }, - { name = "runloop-api-client", marker = "extra == 'runloop'", specifier = ">=1.16.0,<2.0.0" }, { name = "sqlalchemy", extras = ["asyncio"], marker = "extra == 'sqlalchemy'", specifier = ">=2.0" }, { name = "starlette", specifier = ">=1.3.1" }, { name = "temporalio", marker = "extra == 'temporal'", specifier = "==1.34.0" }, @@ -2936,7 +2962,7 @@ requires-dist = [ { name = "websockets", marker = "extra == 'realtime'", specifier = ">=15.0,<17" }, { name = "websockets", marker = "extra == 'voice'", specifier = ">=15.0,<17" }, ] -provides-extras = ["voice", "viz", "litellm", "any-llm", "realtime", "sqlalchemy", "encrypt", "redis", "dapr", "mongodb", "docker", "blaxel", "daytona", "cloudflare", "e2b", "modal", "runloop", "vercel", "s3", "temporal"] +provides-extras = ["voice", "viz", "litellm", "any-llm", "realtime", "sqlalchemy", "encrypt", "redis", "dapr", "mongodb", "docker", "blaxel", "daytona", "cloudflare", "e2b", "modal", "vercel", "s3", "temporal"] [package.metadata.requires-dev] dev = [ @@ -2957,11 +2983,11 @@ dev = [ { name = "mkdocs-static-i18n", specifier = ">=1.3.0" }, { name = "mkdocstrings", extras = ["python"], specifier = ">=0.28.0" }, { name = "mypy" }, - { name = "playwright", specifier = "==1.50.0" }, + { name = "playwright", specifier = "==1.63.0" }, { name = "protobuf", specifier = ">=5.29.6,!=6.30.*,!=6.31.*,!=6.32.*,!=6.33.0.*,!=6.33.1.*,!=6.33.2.*,!=6.33.3.*,!=6.33.4.*" }, { name = "pygments", specifier = ">=2.20.0" }, { name = "pymdown-extensions", specifier = ">=11.0.1" }, - { name = "pymongo", specifier = ">=4.14" }, + { name = "pymongo", specifier = ">=4.18.3" }, { name = "pynput" }, { name = "pyright", specifier = "==1.1.408" }, { name = "pytest", specifier = ">=9.0.3" }, @@ -2969,7 +2995,7 @@ dev = [ { name = "pytest-mock", specifier = ">=3.14.0" }, { name = "pytest-xdist" }, { name = "rich", specifier = ">=13.1.0,<15" }, - { name = "ruff", specifier = "==0.16.8" }, + { name = "ruff", specifier = "==0.17.1" }, { name = "sounddevice" }, { name = "textual" }, { name = "twilio", specifier = ">=9,<10" }, @@ -2991,50 +3017,80 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.40.0" +version = "1.45.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "importlib-metadata" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2c/1d/4049a9e8698361cc1a1aa03a6c59e4fa4c71e0c0f94a30f988a6876a2ae6/opentelemetry_api-1.40.0.tar.gz", hash = "sha256:159be641c0b04d11e9ecd576906462773eb97ae1b657730f0ecf64d32071569f", size = 70851, upload-time = "2026-03-04T14:17:21.555Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/02/6e0ae9cc61bd3169d401077b507b3ebc344745171e1051ab430be012dcd9/opentelemetry_api-1.45.1.tar.gz", hash = "sha256:aa38ed19bcc084ba42782a73255b3582283eced7ad6dddbd6695189e69adfb75", size = 72804, upload-time = "2026-10-06T17:32:58.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/41/f7dcf80b81ee8e71c1a2b59f14208bc723edbd89ed027a73b175abf6348e/opentelemetry_api-1.45.1-py3-none-any.whl", hash = "sha256:b31553efa588ae44bc306f863c785c5333a9ecc091248c6ee68b4b6c87fdedfb", size = 60256, upload-time = "2026-10-06T17:32:33.506Z" }, +] + +[[package]] +name = "opentelemetry-exporter-http-transport" +version = "0.66b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/0c/e3ebdb4b507f66afcc905e6885a4946969bd75b45988492643356fbbdc63/opentelemetry_exporter_http_transport-0.66b1.tar.gz", hash = "sha256:443080203bf52586ce0b2ad901e8951c61833eab1aa539ae6f1f16fe9e8e7952", size = 11693, upload-time = "2026-10-06T17:32:59.65Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/69/6af86ff66492b481c6a4c05dcfd68beb47ed8ba046440a26a2aac76b95c7/opentelemetry_exporter_http_transport-0.66b1-py3-none-any.whl", hash = "sha256:2f95404bdee7f9d2d529c7de56c7bd86d014d774d8fbf137810e0167f8a492bf", size = 12155, upload-time = "2026-10-06T17:32:35.454Z" }, +] + +[package.optional-dependencies] +requests = [ + { name = "requests" }, +] + +[[package]] +name = "opentelemetry-exporter-otlp-common" +version = "0.66b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/19/41de712173f43057e4532d42ece7d0c6d4210d353e5752433cb14987643f/opentelemetry_exporter_otlp_common-0.66b1.tar.gz", hash = "sha256:6b1403487a2185ac1feb45fd5546fdf8630ce71c36bcefaadf51e2130e9e23f9", size = 14325, upload-time = "2026-10-06T17:33:01.725Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/bf/93795954016c522008da367da292adceed71cca6ee1717e1d64c83089099/opentelemetry_api-1.40.0-py3-none-any.whl", hash = "sha256:82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9", size = 68676, upload-time = "2026-03-04T14:17:01.24Z" }, + { url = "https://files.pythonhosted.org/packages/fc/39/8c23d67665c762aa51840fa06f86e902e8f6f1693bc8d7e3d98cd6e2f753/opentelemetry_exporter_otlp_common-0.66b1-py3-none-any.whl", hash = "sha256:00ff8592c3a7cb729ff3fdc7ffa12372c243bdf2163e80c180994d0c7bd83ee9", size = 12385, upload-time = "2026-10-06T17:32:38.177Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.40.0" +version = "1.45.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/51/bc/1559d46557fe6eca0b46c88d4c2676285f1f3be2e8d06bb5d15fbffc814a/opentelemetry_exporter_otlp_proto_common-1.40.0.tar.gz", hash = "sha256:1cbee86a4064790b362a86601ee7934f368b81cd4cc2f2e163902a6e7818a0fa", size = 20416, upload-time = "2026-03-04T14:17:23.801Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c1/8e/65e85e5137991a3c493b11682151d198638a5bc1dd4b4c5f67e013c57d7c/opentelemetry_exporter_otlp_proto_common-1.45.1.tar.gz", hash = "sha256:2e4adcc3a67bcf57804fc49514f0ef64974ca7590aa3491da389852b4a0628f6", size = 18873, upload-time = "2026-10-06T17:33:04.471Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/ca/8f122055c97a932311a3f640273f084e738008933503d0c2563cd5d591fc/opentelemetry_exporter_otlp_proto_common-1.40.0-py3-none-any.whl", hash = "sha256:7081ff453835a82417bf38dccf122c827c3cbc94f2079b03bba02a3165f25149", size = 18369, upload-time = "2026-03-04T14:17:04.796Z" }, + { url = "https://files.pythonhosted.org/packages/84/aa/92f225d353904e7f70b8b3e3c1b02db0cf56f744c2e83c581dc372e78873/opentelemetry_exporter_otlp_proto_common-1.45.1-py3-none-any.whl", hash = "sha256:2f446183ae7047b036226f1d846c41a834b0e8755ad13b51a51dd38952eb466c", size = 15393, upload-time = "2026-10-06T17:32:41.911Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-http" -version = "1.40.0" +version = "1.45.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-http-transport", extra = ["requests"] }, + { name = "opentelemetry-exporter-otlp-common" }, { name = "opentelemetry-exporter-otlp-proto-common" }, { name = "opentelemetry-proto" }, { name = "opentelemetry-sdk" }, { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/fa/73d50e2c15c56be4d000c98e24221d494674b0cc95524e2a8cb3856d95a4/opentelemetry_exporter_otlp_proto_http-1.40.0.tar.gz", hash = "sha256:db48f5e0f33217588bbc00274a31517ba830da576e59503507c839b38fa0869c", size = 17772, upload-time = "2026-03-04T14:17:25.324Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/17/26487707ea4caa97b17e6e4b5fa72133a53512ffa2f5cf7a49ef284b29cb/opentelemetry_exporter_otlp_proto_http-1.45.1.tar.gz", hash = "sha256:45c218405ce3fd879596924b1874bf9a8f6880206d61065c5a912c8e5c297fb7", size = 28839, upload-time = "2026-10-06T17:33:05.713Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/3a/8865d6754e61c9fb170cdd530a124a53769ee5f740236064816eb0ca7301/opentelemetry_exporter_otlp_proto_http-1.40.0-py3-none-any.whl", hash = "sha256:a8d1dab28f504c5d96577d6509f80a8150e44e8f45f82cdbe0e34c99ab040069", size = 19960, upload-time = "2026-03-04T14:17:07.153Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1f/517eaa0187ba106a9da97160ce2add3a371812681dc440930b267f714e42/opentelemetry_exporter_otlp_proto_http-1.45.1-py3-none-any.whl", hash = "sha256:24a97cf3753c7fb52fad44a696e452ff371686339e2acf3309e2eda3d0230700", size = 22180, upload-time = "2026-10-06T17:32:43.946Z" }, ] [[package]] name = "opentelemetry-instrumentation" -version = "0.61b0" +version = "0.66b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -3042,14 +3098,14 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/37/6bf8e66bfcee5d3c6515b79cb2ee9ad05fe573c20f7ceb288d0e7eeec28c/opentelemetry_instrumentation-0.61b0.tar.gz", hash = "sha256:cb21b48db738c9de196eba6b805b4ff9de3b7f187e4bbf9a466fa170514f1fc7", size = 32606, upload-time = "2026-03-04T14:20:16.825Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/03/89e47ff8d52a4f83b343e6eb9ef1698ff45357216e5b6b2b21e0da5c5c7d/opentelemetry_instrumentation-0.66b1.tar.gz", hash = "sha256:e79a510f7d87c72d95e964ddb42193a0d9a75668c027d980eab032ea1322a5ce", size = 43308, upload-time = "2026-10-06T17:36:10.703Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/3e/f6f10f178b6316de67f0dfdbbb699a24fbe8917cf1743c1595fb9dcdd461/opentelemetry_instrumentation-0.61b0-py3-none-any.whl", hash = "sha256:92a93a280e69788e8f88391247cc530fd81f16f2b011979d4d6398f805cfbc63", size = 33448, upload-time = "2026-03-04T14:19:02.447Z" }, + { url = "https://files.pythonhosted.org/packages/da/b2/d1413681ff43e13ac9860df27e1226d3199ab0b97b352ceea41abcc660a5/opentelemetry_instrumentation-0.66b1-py3-none-any.whl", hash = "sha256:4c4aa14dc9a24a02325a9d4c42c4d0208dbb1374c2b1b8fe6c9392d59f3e1008", size = 36904, upload-time = "2026-10-06T17:35:11.663Z" }, ] [[package]] name = "opentelemetry-instrumentation-aiohttp-client" -version = "0.61b0" +version = "0.66b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -3058,57 +3114,57 @@ dependencies = [ { name = "opentelemetry-util-http" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/24fed4de661de107f2426b28bbd87b51eaab28a2339b62f269a36ae24505/opentelemetry_instrumentation_aiohttp_client-0.61b0.tar.gz", hash = "sha256:c53ab3b88efcb7ce98c1129cc0389f0a1f214eb3675269b6c157770adcf47877", size = 19292, upload-time = "2026-03-04T14:20:18.408Z" } +sdist = { url = "https://files.pythonhosted.org/packages/85/fe/634d77a0109df609eb290d172ab57e45fe35a1ef2c277f3e1908267acaa1/opentelemetry_instrumentation_aiohttp_client-0.66b1.tar.gz", hash = "sha256:6f853dadc53916fd6af41413bcb627b3e36c4a01b581e03b61b2f8eb6031d69f", size = 18849, upload-time = "2026-10-06T17:36:12.072Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/f3/1edc42716521a3f754ac32ffb908f102e0f131f8e43fcd9ab29cab286723/opentelemetry_instrumentation_aiohttp_client-0.61b0-py3-none-any.whl", hash = "sha256:09bc47514c162507b357366ce15578743fd6305078cf7d872db1c99c13fa6972", size = 14534, upload-time = "2026-03-04T14:19:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/7b/30/dd352a722a4b928b22363df249b38b6d173173d36b2eab6d04770fd7d348/opentelemetry_instrumentation_aiohttp_client-0.66b1-py3-none-any.whl", hash = "sha256:bb3d412ae30e83e0692c8e9b600c5990bdab736c7c048847461b4567d504078d", size = 13610, upload-time = "2026-10-06T17:35:13.651Z" }, ] [[package]] name = "opentelemetry-proto" -version = "1.40.0" +version = "1.45.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4c/77/dd38991db037fdfce45849491cb61de5ab000f49824a00230afb112a4392/opentelemetry_proto-1.40.0.tar.gz", hash = "sha256:03f639ca129ba513f5819810f5b1f42bcb371391405d99c168fe6937c62febcd", size = 45667, upload-time = "2026-03-04T14:17:31.194Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4b/7f/15f014fb195da6c2dbb6c71399b8e76824878718e94de6454038488eed28/opentelemetry_proto-1.45.1.tar.gz", hash = "sha256:79e0fb95e4616691a469439238aa9224d75779b3e108e895d1aa125ab29ca77c", size = 46488, upload-time = "2026-10-06T17:33:11.49Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/b2/189b2577dde745b15625b3214302605b1353436219d42b7912e77fa8dc24/opentelemetry_proto-1.40.0-py3-none-any.whl", hash = "sha256:266c4385d88923a23d63e353e9761af0f47a6ed0d486979777fe4de59dc9b25f", size = 72073, upload-time = "2026-03-04T14:17:16.673Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9a/42ec8180a769516ae757e893b69736826efceac7332553915b4528a91c6d/opentelemetry_proto-1.45.1-py3-none-any.whl", hash = "sha256:f38e2a8413053c180cd3d2637fbb279673ec2f6a6e09c995aafa2f452c52b46e", size = 72488, upload-time = "2026-10-06T17:32:53.057Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.40.0" +version = "1.45.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/58/fd/3c3125b20ba18ce2155ba9ea74acb0ae5d25f8cd39cfd37455601b7955cc/opentelemetry_sdk-1.40.0.tar.gz", hash = "sha256:18e9f5ec20d859d268c7cb3c5198c8d105d073714db3de50b593b8c1345a48f2", size = 184252, upload-time = "2026-03-04T14:17:31.87Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/79/7392e21a1c8f0c61d90b223e31c7e48cb9d452e91a6b820ad24cca5f23c4/opentelemetry_sdk-1.45.1.tar.gz", hash = "sha256:63d24a6ca645019a631e6a51999c73e93adcac1196ca640b8ae78a7cc4762bf3", size = 218324, upload-time = "2026-10-06T17:33:13.26Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/c5/6a852903d8bfac758c6dc6e9a68b015d3c33f2f1be5e9591e0f4b69c7e0a/opentelemetry_sdk-1.40.0-py3-none-any.whl", hash = "sha256:787d2154a71f4b3d81f20524a8ce061b7db667d24e46753f32a7bc48f1c1f3f1", size = 141951, upload-time = "2026-03-04T14:17:17.961Z" }, + { url = "https://files.pythonhosted.org/packages/95/3c/87c42b4bd6dd297536f04cd9383d212ac557ecd49f2cbdcd46da1c9ef5c8/opentelemetry_sdk-1.45.1-py3-none-any.whl", hash = "sha256:c604c11dc429810812348989115fa44bd558772a3d7442afc43d024f2c250ca4", size = 140063, upload-time = "2026-10-06T17:32:55.04Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.61b0" +version = "0.66b1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6d/c0/4ae7973f3c2cfd2b6e321f1675626f0dab0a97027cc7a297474c9c8f3d04/opentelemetry_semantic_conventions-0.61b0.tar.gz", hash = "sha256:072f65473c5d7c6dc0355b27d6c9d1a679d63b6d4b4b16a9773062cb7e31192a", size = 145755, upload-time = "2026-03-04T14:17:32.664Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/e4/dbbfb2a010c4db2224a5114638acede6fe563d33cc20fb1752cebcbe6298/opentelemetry_semantic_conventions-0.66b1.tar.gz", hash = "sha256:497ca63bf383723411e8eaf60c8779e9877633c936bb641080adab59d0eb6ec8", size = 150250, upload-time = "2026-10-06T17:33:14.073Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/37/cc6a55e448deaa9b27377d087da8615a3416d8ad523d5960b78dbeadd02a/opentelemetry_semantic_conventions-0.61b0-py3-none-any.whl", hash = "sha256:fa530a96be229795f8cef353739b618148b0fe2b4b3f005e60e262926c4d38e2", size = 231621, upload-time = "2026-03-04T14:17:19.33Z" }, + { url = "https://files.pythonhosted.org/packages/bc/14/67f8aa798857f8cf686f515bf93d9bb877ce952ddc8efae0fa25b45ce0d6/opentelemetry_semantic_conventions-0.66b1-py3-none-any.whl", hash = "sha256:d4cddeb4315490b35213f55e2bdc9ac54bb1e4d318927475bed62b35545e581b", size = 206279, upload-time = "2026-10-06T17:32:56.103Z" }, ] [[package]] name = "opentelemetry-util-http" -version = "0.61b0" +version = "0.66b1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/3c/f0196223efc5c4ca19f8fad3d5462b171ac6333013335ce540c01af419e9/opentelemetry_util_http-0.61b0.tar.gz", hash = "sha256:1039cb891334ad2731affdf034d8fb8b48c239af9b6dd295e5fabd07f1c95572", size = 11361, upload-time = "2026-03-04T14:20:57.01Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/b5/df4b61da899f6ebdffdbdf0c8b0f3189ee57151694ccd5b7d50ee2906241/opentelemetry_util_http-0.66b1.tar.gz", hash = "sha256:047dea1a628031f857a5a32261dc0e955bc162d39993ed1cffb8f2cff5ba8a62", size = 11586, upload-time = "2026-10-06T17:36:46.572Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/e5/c08aaaf2f64288d2b6ef65741d2de5454e64af3e050f34285fb1907492fe/opentelemetry_util_http-0.61b0-py3-none-any.whl", hash = "sha256:8e715e848233e9527ea47e275659ea60a57a75edf5206a3b937e236a6da5fc33", size = 9281, upload-time = "2026-03-04T14:20:08.364Z" }, + { url = "https://files.pythonhosted.org/packages/eb/9b/c77ecaea79ba0de1a11e7f06a7f5eea7043ec23f1860dcf5f03536698e4c/opentelemetry_util_http-0.66b1-py3-none-any.whl", hash = "sha256:8f443d7abcaf29c4a07b373bbd31b5b39132c0ed3c27d015a59dc0323d5b1c58", size = 8387, upload-time = "2026-10-06T17:36:06.984Z" }, ] [[package]] @@ -3149,20 +3205,21 @@ wheels = [ [[package]] name = "playwright" -version = "1.50.0" +version = "1.63.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet" }, { name = "pyee" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/5e/068dea3c96e9c09929b45c92cf7e573403b52a89aa463f89b9da9b87b7a4/playwright-1.50.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:f36d754a6c5bd9bf7f14e8f57a2aea6fd08f39ca4c8476481b9c83e299531148", size = 40277564, upload-time = "2025-02-03T14:57:22.774Z" }, - { url = "https://files.pythonhosted.org/packages/78/85/b3deb3d2add00d2a6ee74bf6f57ccefb30efc400fd1b7b330ba9a3626330/playwright-1.50.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:40f274384591dfd27f2b014596250b2250c843ed1f7f4ef5d2960ecb91b4961e", size = 39521844, upload-time = "2025-02-03T14:57:29.372Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f6/002b3d98df9c84296fea84f070dc0d87c2270b37f423cf076a913370d162/playwright-1.50.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:9922ef9bcd316995f01e220acffd2d37a463b4ad10fd73e388add03841dfa230", size = 40277563, upload-time = "2025-02-03T14:57:36.291Z" }, - { url = "https://files.pythonhosted.org/packages/b9/63/c9a73736e434df894e484278dddc0bf154312ff8d0f16d516edb790a7d42/playwright-1.50.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:8fc628c492d12b13d1f347137b2ac6c04f98197ff0985ef0403a9a9ee0d39131", size = 45076712, upload-time = "2025-02-03T14:57:43.581Z" }, - { url = "https://files.pythonhosted.org/packages/bd/2c/a54b5a64cc7d1a62f2d944c5977fb3c88e74d76f5cdc7966e717426bce66/playwright-1.50.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcff35f72db2689a79007aee78f1b0621a22e6e3d6c1f58aaa9ac805bf4497c", size = 44493111, upload-time = "2025-02-03T14:57:50.226Z" }, - { url = "https://files.pythonhosted.org/packages/2b/4a/047cbb2ffe1249bd7a56441fc3366fb4a8a1f44bc36a9061d10edfda2c86/playwright-1.50.0-py3-none-win32.whl", hash = "sha256:3b906f4d351260016a8c5cc1e003bb341651ae682f62213b50168ed581c7558a", size = 34784543, upload-time = "2025-02-03T14:57:55.942Z" }, - { url = "https://files.pythonhosted.org/packages/bc/2b/e944e10c9b18e77e43d3bb4d6faa323f6cc27597db37b75bc3fd796adfd5/playwright-1.50.0-py3-none-win_amd64.whl", hash = "sha256:1859423da82de631704d5e3d88602d755462b0906824c1debe140979397d2e8d", size = 34784546, upload-time = "2025-02-03T14:58:01.664Z" }, + { url = "https://files.pythonhosted.org/packages/19/dd/fbb3d34228ad753bc14464d5f2252585b73367539775a88d0a3949cf5a45/playwright-1.63.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:84c540759e8e7f01e690e197e04060f48899d37cea322299f255843273d3385d", size = 44278407, upload-time = "2026-09-15T16:49:06.045Z" }, + { url = "https://files.pythonhosted.org/packages/f5/9a/948b930b1a8c4ee869e5a139a2b7747caa06aab56a3f09a2f0abdcbda221/playwright-1.63.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fd1aa00631d44d55e56e0975bf3f3f285fac4a9fd2813183f0c12a488f1a1b24", size = 42944679, upload-time = "2026-09-15T16:49:10.039Z" }, + { url = "https://files.pythonhosted.org/packages/94/11/dc5c13fa1602371603acd461be47529c1b3513815d3a0dc98f642c291a10/playwright-1.63.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:c89fc4736502a1f0fac2c8ca5d10c0cbc1c669f1f4774a2d8507a43140e4d53f", size = 44278410, upload-time = "2026-09-15T16:49:13.861Z" }, + { url = "https://files.pythonhosted.org/packages/27/9c/103a5037789062bdab27c7dca53f3ca6b075b572ab2cd96eec825b3aec4e/playwright-1.63.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:ad21bc07516b187965a7521c5cf0df0bd657b17482eaad74335272d35a2b07de", size = 48217159, upload-time = "2026-09-15T16:49:17.404Z" }, + { url = "https://files.pythonhosted.org/packages/f3/82/3d85505284c5a210f2da6c07b8f757524e79d1fba9cfdafe1eafb766ae59/playwright-1.63.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:354e15b29503565fc598b89f16fbe070459343bef9d7498a93e304864000c6a7", size = 47902708, upload-time = "2026-09-15T16:49:21.055Z" }, + { url = "https://files.pythonhosted.org/packages/69/8d/f74ff6b52751859f4b69caf66aa7d4a3c8d6c1f0c7dc9920da103612ee39/playwright-1.63.0-py3-none-win32.whl", hash = "sha256:660c00c62639e31b16700ba5456b351ddba55bb766b7ce8261223aa34928e482", size = 38606075, upload-time = "2026-09-15T16:49:29.546Z" }, + { url = "https://files.pythonhosted.org/packages/76/eb/d6b8d92658038e260dbc7dd69fb3fdbe245aac283953de8b50b02bfe5f61/playwright-1.63.0-py3-none-win_amd64.whl", hash = "sha256:2f9a707a6c6c91157ed77bff2b8caeb04b3c8d46e70d585fc134298cbe4b5cc6", size = 38606082, upload-time = "2026-09-15T16:49:32.838Z" }, + { url = "https://files.pythonhosted.org/packages/be/75/2432b3e3c7c62103b72d4c4cc8b16a56383ada372bbb0d1278591e988f71/playwright-1.63.0-py3-none-win_arm64.whl", hash = "sha256:1e4a3a838ce22fb68ad17193fcd142a19610d9d70fb9966d2239f5dddc0cc05b", size = 34523554, upload-time = "2026-09-15T16:49:36.455Z" }, ] [[package]] @@ -3418,14 +3475,14 @@ wheels = [ [[package]] name = "pyee" -version = "12.1.1" +version = "13.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0a/37/8fb6e653597b2b67ef552ed49b438d5398ba3b85a9453f8ada0fd77d455c/pyee-12.1.1.tar.gz", hash = "sha256:bbc33c09e2ff827f74191e3e5bbc6be7da02f627b7ec30d86f5ce1a6fb2424a3", size = 30915, upload-time = "2024-11-16T21:26:44.275Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/04/e7c1fe4dc78a6fdbfd6c337b1c3732ff543b8a397683ab38378447baa331/pyee-13.0.1.tar.gz", hash = "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8", size = 31655, upload-time = "2026-02-14T21:12:28.044Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/25/68/7e150cba9eeffdeb3c5cecdb6896d70c8edd46ce41c0491e12fb2b2256ff/pyee-12.1.1-py3-none-any.whl", hash = "sha256:18a19c650556bb6b32b406d7f017c8f513aceed1ef7ca618fb65de7bd2d347ef", size = 15527, upload-time = "2024-11-16T21:26:42.422Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c4/b4d4827c93ef43c01f599ef31453ccc1c132b353284fc6c87d535c233129/pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228", size = 15659, upload-time = "2026-02-14T21:12:26.263Z" }, ] [[package]] @@ -3469,73 +3526,73 @@ wheels = [ [[package]] name = "pymongo" -version = "4.18.2" +version = "4.18.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dnspython" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dc/04/455039b475bb8afa5af9b8c363fb773a45d898016f04ee40c1c75003a3f9/pymongo-4.18.2.tar.gz", hash = "sha256:1bd4321cc63c06954953eae9eb8fb5d9075296b96c0e3ed34ccb91a0dcce31a5", size = 2746691, upload-time = "2026-09-24T16:59:59.472Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/19/fe076a243faa8bc8f41d5ecc7c3dd625437bbca6b0dae633e05e8811e6db/pymongo-4.18.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb4892f6edd939677e767e45087173c7292cfcfa0ea52fb2076e1fb89fbd597f", size = 819557, upload-time = "2026-09-24T16:58:04.8Z" }, - { url = "https://files.pythonhosted.org/packages/23/ad/be444b903d5e712b659ac83b23643e2df2b61631f46d7ece02521caeb7fc/pymongo-4.18.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f953e62a054030c7c7f923f65da95cb5332230a135ee24e57af2defad7d8cff", size = 820096, upload-time = "2026-09-24T16:58:06.883Z" }, - { url = "https://files.pythonhosted.org/packages/c9/83/9e6b7364e9234689fc17c0897bbac95b6bb032d8d16d6070c4a617c06cf9/pymongo-4.18.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:69de0a25ce088e45c25b4ba3e84ad7f530375b48f6024dda48b1ec34a08b4d4f", size = 1041104, upload-time = "2026-09-24T16:58:08.468Z" }, - { url = "https://files.pythonhosted.org/packages/21/6a/02f792efaed73e56910bbdf3b161c0b85b651807d6a5ab498b60c2637329/pymongo-4.18.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3fb1f90fcd5aec21535ace955bb72195751c09ea179d751855f7a066a6fb8ab", size = 1049685, upload-time = "2026-09-24T16:58:09.873Z" }, - { url = "https://files.pythonhosted.org/packages/a0/fe/3da97617e4a5c4144429308498507b326332d2271025ca23bbd8fe5b1f99/pymongo-4.18.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b2dff5ef3d523caa47ca06e6832578b463e05e2ffb74d637fe633305680ab9d", size = 1074077, upload-time = "2026-09-24T16:58:11.321Z" }, - { url = "https://files.pythonhosted.org/packages/0c/95/1731d74417f1f298f004fe4e9dae19a326e8e743478a4006afab43682ec5/pymongo-4.18.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078b73765963f716e12a7ef9f2247baef22f01c0a7288281cf8fe18ca0f96635", size = 1067691, upload-time = "2026-09-24T16:58:12.747Z" }, - { url = "https://files.pythonhosted.org/packages/7c/6d/cf29ce0b02f38946e8f6e55d232cbb34b6e346c9b3874427bf2c276b7ccf/pymongo-4.18.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46d9922c08c0fc166620b5693829a2852a16082f4fc8fb431e30bf6d0a1f4984", size = 1051651, upload-time = "2026-09-24T16:58:14.26Z" }, - { url = "https://files.pythonhosted.org/packages/bb/ca/425a20beb371f3e7d379425bbbf97496632dbcf0d7b5c4fd2f325435f8b3/pymongo-4.18.2-cp310-cp310-win32.whl", hash = "sha256:899bd9012e72eeec7616456f13f9402f96ed4e0ec110974cf9055082b4c2bb78", size = 814456, upload-time = "2026-09-24T16:58:15.897Z" }, - { url = "https://files.pythonhosted.org/packages/e4/d9/4082e0fe625624bd71d53792cc49b466536425cba75d9e56fff6972d1a2d/pymongo-4.18.2-cp310-cp310-win_amd64.whl", hash = "sha256:c80e4baee9746f1239cc5b83bb983c01a04769761adb8f462ce0a1902f7cd8bd", size = 821727, upload-time = "2026-09-24T16:58:17.395Z" }, - { url = "https://files.pythonhosted.org/packages/f8/b3/c6f190cc19b873d9aa9e261ee184ae5a605e1d21d4bd3105aa9d52943f86/pymongo-4.18.2-cp310-cp310-win_arm64.whl", hash = "sha256:045f16e6b87b832b37f3acf487c48308bc6bb171bc8b4a3e97f156cf07de14b4", size = 815866, upload-time = "2026-09-24T16:58:19.258Z" }, - { url = "https://files.pythonhosted.org/packages/65/01/e8358c562ce4e3255035f16eed43a6f77669b0779f3aa918362c670e869f/pymongo-4.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257519b59d8509a950b21c4a626e5295b3ad4397c12ec47e6e93353c49236145", size = 818562, upload-time = "2026-09-24T16:58:20.602Z" }, - { url = "https://files.pythonhosted.org/packages/1f/18/48a9e7f44a35560284b36bba24622a959ec1dd940e7c4f7cd308cfdba756/pymongo-4.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:845b890622e7a2f031140f9cb1c0f266c1baa876ea359f7e51d0c38112cb3c6e", size = 819081, upload-time = "2026-09-24T16:58:22.043Z" }, - { url = "https://files.pythonhosted.org/packages/fc/24/2d9c0bb5649f262257f507b03f84419ac1254e94894cd493bda87fb935e1/pymongo-4.18.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a746f7e183c6d48aba6152d9f333e3bae2e8b6b00fac7abe69d4bdb3d10915c8", size = 1023284, upload-time = "2026-09-24T16:58:23.437Z" }, - { url = "https://files.pythonhosted.org/packages/70/40/af888435ce8e3ae1e1a201b87fb397aa91e3ce115bb1bb3999197f2b9b72/pymongo-4.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf9d13d812e55d406080b8ec3d5b9ef8eb2279bac32529e4f7b41cfefd5340e2", size = 1031549, upload-time = "2026-09-24T16:58:24.869Z" }, - { url = "https://files.pythonhosted.org/packages/3c/66/f5053862cbeddf37b3184870fa179a3e9d9e27e668c720bb7ab28b65b753/pymongo-4.18.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:930e8bcdf781a73d3df05976f5eaca1193102571cb91726e30009d64878ef8ce", size = 1054025, upload-time = "2026-09-24T16:58:26.229Z" }, - { url = "https://files.pythonhosted.org/packages/70/29/4d7c9d7cb718ea7bbac56465de3aea5f79d1266683c16bdcd40e77746530/pymongo-4.18.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a695bd34d1e942eca93a186e679be196e288370f1a203e383b2187c06349e348", size = 1046337, upload-time = "2026-09-24T16:58:27.822Z" }, - { url = "https://files.pythonhosted.org/packages/d8/aa/c18c564883c5b2616744d1ee7ab073a3b4045aaa4bbbd524105448d4f630/pymongo-4.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d95e339e94b82950bfd62ec9ff8fea9bc8dfd9db0940e9134d1647d366560df5", size = 1032845, upload-time = "2026-09-24T16:58:29.518Z" }, - { url = "https://files.pythonhosted.org/packages/20/16/f8a1cb7ac3f1c2c45649f18b7cb398779b4b45e1954bbfcb9317c74c744d/pymongo-4.18.2-cp311-cp311-win32.whl", hash = "sha256:2e2a3b816270c493f6645a168150ecbfb19a0bd0d9d3b56be728a847954da717", size = 814203, upload-time = "2026-09-24T16:58:30.924Z" }, - { url = "https://files.pythonhosted.org/packages/04/53/e9d6350ea753b7a72d0346fb2c8e0c589e0eb443b5041989f3b4ea52ce55/pymongo-4.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:812b4ad5fe577f829cdb4e39a5601025b9cd7f2e3f8cd373cd5ab64ce78b0fb0", size = 821265, upload-time = "2026-09-24T16:58:32.479Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2a/be9693eca04bb6f53f3bdb027220a210bd429c7bf902b79538471849297e/pymongo-4.18.2-cp311-cp311-win_arm64.whl", hash = "sha256:c36cadbc3a7712575900c6f50cdf24ebe2b376b6ad643cb5df1b3dce93447eae", size = 815444, upload-time = "2026-09-24T16:58:34.212Z" }, - { url = "https://files.pythonhosted.org/packages/10/eb/995ec978741cfc56a29223ec0806976218b95d79a516d9b719c984ead130/pymongo-4.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78a3b324407ba7228a57f207e175efc40341c1312d1e7df2d4ad8141ff6d8e00", size = 819057, upload-time = "2026-09-24T16:58:35.844Z" }, - { url = "https://files.pythonhosted.org/packages/40/e4/5a166bbac7f6e620cfb75b0640e793dd3ce69423a1f89cf6a0bfc588c107/pymongo-4.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:498187b5171d5efa87291b30e4cbf1e9849929c02bc436e95a3d01ff19e49f42", size = 819448, upload-time = "2026-09-24T16:58:37.221Z" }, - { url = "https://files.pythonhosted.org/packages/c9/c8/82194d9a1387a56cac996901f92ea6a25ae817622ee32d15c986638b263f/pymongo-4.18.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2c003646d3fa5db4e835483fc29018eb06d8c637cf69f676ffbb3f0734005220", size = 1039602, upload-time = "2026-09-24T16:58:38.821Z" }, - { url = "https://files.pythonhosted.org/packages/cc/06/628f93f15f269d8a1ea20a024083e67871f722876a33992aec5525a6c6c4/pymongo-4.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c743347143298252948e038f768ea6f71b71ef8a85b7cf0dc13b734e072f74da", size = 1050746, upload-time = "2026-09-24T16:58:40.322Z" }, - { url = "https://files.pythonhosted.org/packages/bf/93/58ea76803128651ec9c732b59b8c1eb73d511c085c2e5786da3289d4015c/pymongo-4.18.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:09e6afa68001433c42925a258b57f2ceccb95c73fe8920452e147a82d90c1ae9", size = 1075681, upload-time = "2026-09-24T16:58:41.722Z" }, - { url = "https://files.pythonhosted.org/packages/7c/6d/702ee4593420e2a8ceb169ece6394f0bb9fc2e8e8d8101e99f2468ed6d0d/pymongo-4.18.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8d3445caeeb47dc64bf416dc03e0b3bfcf3b60a109dec355a563aa62c135cb33", size = 1068235, upload-time = "2026-09-24T16:58:43.228Z" }, - { url = "https://files.pythonhosted.org/packages/a9/92/a02a4108ca4faf05bca394d65dcebc7b84b0248cff23aa89927c81a0d5dd/pymongo-4.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecde9e735e1f86559e0a71a5bbec671e59a35549bfd8c60968d9f032b05013de", size = 1049896, upload-time = "2026-09-24T16:58:44.914Z" }, - { url = "https://files.pythonhosted.org/packages/6e/93/65ef17eecdd86c153f7907b2d490d85eb655d3a5eb35a8494e7e09376353/pymongo-4.18.2-cp312-cp312-win32.whl", hash = "sha256:a463c3fc072655b7fe9e9ded4601876bef7f1df98f6f3faeed07fa7f11309fff", size = 815124, upload-time = "2026-09-24T16:58:46.39Z" }, - { url = "https://files.pythonhosted.org/packages/3c/cc/fd66aa9d8956e8269ea63b2977a7fd38fbec45795d38b8067bfafc98992f/pymongo-4.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:cbb67901ba9b6334fa94643d66587a30ffe5be05061af287364e6b5f7d239dc5", size = 821996, upload-time = "2026-09-24T16:58:47.769Z" }, - { url = "https://files.pythonhosted.org/packages/06/21/983d340cfe226c987014f1caf9568b176cbd7f25dd87536379a609e6371e/pymongo-4.18.2-cp312-cp312-win_arm64.whl", hash = "sha256:94c5e0fe9af4c05f382ae7c4addd0255a8ddcc91b3d2a6bc09b34230cb28290b", size = 815707, upload-time = "2026-09-24T16:58:49.493Z" }, - { url = "https://files.pythonhosted.org/packages/c7/be/82ca9a311b8a89e918c7387d96135786e1edd2eedca43d62dffcb3af5846/pymongo-4.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3be3633a89888fc7443a5a416f74c66216839a5b7b550daaec4da0b1a7283be", size = 818977, upload-time = "2026-09-24T16:58:51.109Z" }, - { url = "https://files.pythonhosted.org/packages/43/38/4817e5ce653ed70162d1412cc74d15c29e88208cea7510967707e908a145/pymongo-4.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd5ecb6d6fab1c5114e66477b1d60b0f3926975c41cf5508d38b28dd0eb6676e", size = 819319, upload-time = "2026-09-24T16:58:52.703Z" }, - { url = "https://files.pythonhosted.org/packages/f3/e9/b8f108ce7c47619e531e0f4a0963178eb9caa5c5da36f4454df8ae6fd15b/pymongo-4.18.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:63322fa1499d855a6e63673f8d183fc2d7ecd966702f73150b21e04e01dc0144", size = 1039779, upload-time = "2026-09-24T16:58:54.292Z" }, - { url = "https://files.pythonhosted.org/packages/e5/87/3a8394386d485c9c9e518c4e15dce91e9470c3a8b3a0a19e289eb3b9d262/pymongo-4.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:979d1e88a20fe9d652f3db57fff55052ddb9022ac346ea55a32a8cfe4b56f90d", size = 1050543, upload-time = "2026-09-24T16:58:55.907Z" }, - { url = "https://files.pythonhosted.org/packages/68/7d/84962f4d8fe0f9e3f112c6b630c7ad9a9f74acd7865ace9ba1e82e1f096a/pymongo-4.18.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ef9007562b907a0db6b158ae9e08510678991cc10ad1471de3635f9be737a68e", size = 1075500, upload-time = "2026-09-24T16:58:57.424Z" }, - { url = "https://files.pythonhosted.org/packages/7b/4e/b20ffbc001958d597970567e9231664b8f4c68765376a349834577892fe3/pymongo-4.18.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8b41d460c2281874232e6b4a5834bc3fb6cc718d80b6097c38d87ff19ee60898", size = 1067941, upload-time = "2026-09-24T16:58:59.014Z" }, - { url = "https://files.pythonhosted.org/packages/b4/ba/535911b6b35427e3fd31c17a89c9851c024df81779fe6dd473a4f0cdd0fd/pymongo-4.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a57f28271cb325574bd12ad7b3747e545c5b7f560de9f175af7db7be0655639", size = 1049780, upload-time = "2026-09-24T16:59:00.716Z" }, - { url = "https://files.pythonhosted.org/packages/fb/d2/f37bdbbc1a11955dd2609d622aae6722c6cfd9f67528a62c8f80f4db6609/pymongo-4.18.2-cp313-cp313-win32.whl", hash = "sha256:c3f0adbefa29c772f4ebf09a6ac65426405435de199828bedc9534f182ad2fe3", size = 815166, upload-time = "2026-09-24T16:59:02.434Z" }, - { url = "https://files.pythonhosted.org/packages/5a/8a/dc8dac16d8a1c72008a1707046db333a4adaf3dec712c4433ac234e7890d/pymongo-4.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:84f99563560a94321b7c3eb4a3bd0dc81f29a2f67a1da065f78641eff42f4a81", size = 821994, upload-time = "2026-09-24T16:59:04.127Z" }, - { url = "https://files.pythonhosted.org/packages/5c/c9/979b68292e3229a924280ba0c8d5fe4f92604271aa49ba09e497af4aa463/pymongo-4.18.2-cp313-cp313-win_arm64.whl", hash = "sha256:f319857f858a17e1c0e48159b65c89abe93d5ebf3423bb3e3891d8b67a68accd", size = 815730, upload-time = "2026-09-24T16:59:05.629Z" }, - { url = "https://files.pythonhosted.org/packages/ae/05/bbd7cc697e56a92821310b8b8f46f2f3cb787d37e9eb29feb179cb00406a/pymongo-4.18.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1c89bd8b901f2e69e0b129feedcc487f4a526140637dcff4501a535b9e4cbd09", size = 818884, upload-time = "2026-09-24T16:59:07.296Z" }, - { url = "https://files.pythonhosted.org/packages/ec/c9/24b4d3c9ccac4b42729c4c11670de137dfd510fca710470bb74bb9a34854/pymongo-4.18.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d11b4bfe82f0ca45d2305f707ddb9f1edb345b10baed60a0384c1e6bc1ca3778", size = 819450, upload-time = "2026-09-24T16:59:09.224Z" }, - { url = "https://files.pythonhosted.org/packages/1b/c2/32b0d851cb94690053f25eabe859a655174098256a57d13ec04413a61600/pymongo-4.18.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bad9e6898ac71e57dce337bdb5561722bd5a452e8e8ab6ca78afd69ae767761e", size = 1041542, upload-time = "2026-09-24T16:59:10.89Z" }, - { url = "https://files.pythonhosted.org/packages/18/91/242659838210f03059c7822a2f854043dfd7402f33e27a28b78cb42cd926/pymongo-4.18.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01036c697d9bd0f2bd0373f32b76f8f5ee0d412509ef2647ece22a032363e08d", size = 1051247, upload-time = "2026-09-24T16:59:12.816Z" }, - { url = "https://files.pythonhosted.org/packages/4b/c8/76cd0897a16676fa6bc03e0ddea853940157530bb58327b55ad1a399eeb9/pymongo-4.18.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc55d12749c34c79b3b7c366a41653bdbbeb2d6999743fef9445b7f8b328d0c0", size = 1075546, upload-time = "2026-09-24T16:59:14.431Z" }, - { url = "https://files.pythonhosted.org/packages/43/19/5aabaefc4f01db369af0ecf876b7cfbf7d23900f4d10457a803b1689c2dc/pymongo-4.18.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3c43efaca0b1060d3f6c3c8ffa39f5a272959c9eb64fca7718e42d40cac201fa", size = 1064567, upload-time = "2026-09-24T16:59:16.111Z" }, - { url = "https://files.pythonhosted.org/packages/84/85/bcd7aa8a85785e5ab7e99a8f87c988a858b45350dc7f45dd5b91d0d3d200/pymongo-4.18.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c8ac2c5a5dbdac98731e0078fc558066bec8d655f47ac19af8d5f8bf64208d5", size = 1049499, upload-time = "2026-09-24T16:59:17.759Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/b5dcd1475e6bcb5c099f03e1a8dee5f4a599f3b0bffbcada0b60144ee3a6/pymongo-4.18.2-cp314-cp314-win32.whl", hash = "sha256:0095668ed0374687bb4ec3d216c3ab35fce56d06485a4b192e3205c6509de778", size = 816300, upload-time = "2026-09-24T16:59:19.43Z" }, - { url = "https://files.pythonhosted.org/packages/14/35/4c6f1ceec6619de273b8948e7f04dd70941558f527735145920ff517744a/pymongo-4.18.2-cp314-cp314-win_amd64.whl", hash = "sha256:ba5e91646910a9f3292cfaee7a172877f3c74bdbab1994677ba44ee1382be7b5", size = 823500, upload-time = "2026-09-24T16:59:21.231Z" }, - { url = "https://files.pythonhosted.org/packages/af/13/233fa438a5c8f07ee9e33fd1bedb7305f07529c8ec01dfb49362e848f1f2/pymongo-4.18.2-cp314-cp314-win_arm64.whl", hash = "sha256:32e20e17aa7dedf07c53d05b198b70ef7d11b1cabe6248589099f5c99caa388c", size = 816850, upload-time = "2026-09-24T16:59:22.861Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ee/d1899e9db48749b3600bfae3eb6da0d1fe8a789910870202cc55f04a35be/pymongo-4.18.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57e7475f2122a70f19030caba0c20f131a5ddd4c55eebecaa75173d6a655a3f1", size = 821863, upload-time = "2026-09-24T16:59:24.459Z" }, - { url = "https://files.pythonhosted.org/packages/31/f9/5a48db5c3734813fed15e78f557eb5bce35347bfaef3da31a237d0586d47/pymongo-4.18.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2ed31b3ed297ce1d49affbaca03cc3ab52604c7e04c10a6d9699f65a62310f7d", size = 822287, upload-time = "2026-09-24T16:59:26.019Z" }, - { url = "https://files.pythonhosted.org/packages/be/1a/9f0b9f7992c710a9bc5f974c2a5d8f9042d7fb8f719342fd624607c6e26d/pymongo-4.18.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8670ff0cba9a021e99d40e86ef59537b7ae81a3617a2e7c10ebbc974ef42a379", size = 1105891, upload-time = "2026-09-24T16:59:27.896Z" }, - { url = "https://files.pythonhosted.org/packages/df/f0/3603c875adf6108e71c1e754f4a3901a7b57cda226ec91c51de55b5950e0/pymongo-4.18.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21b5f1460ffa1d9f7fcd8f974c9d3dc2d6ff29433a55357edb97545efee9b70d", size = 1125444, upload-time = "2026-09-24T16:59:29.616Z" }, - { url = "https://files.pythonhosted.org/packages/a1/88/75b9f4095c5b90093c759d1a3831d698c2b58492d32c8be52a248923f51e/pymongo-4.18.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cd55a76ebbc0ae6504235224127be5549bd8ffa467acecc2a6fca82b8a155ddd", size = 1145149, upload-time = "2026-09-24T16:59:31.543Z" }, - { url = "https://files.pythonhosted.org/packages/8e/d4/5ed7320a48cbf971092727e48f106d667fe6e9682aebcc4732e18a66ddc1/pymongo-4.18.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:39652ef4457fda30124873bd51080609ccd566edcc1ec81aa72614b0bc7e48db", size = 1136527, upload-time = "2026-09-24T16:59:33.85Z" }, - { url = "https://files.pythonhosted.org/packages/41/be/48522b884df7c901a657ac7d26d9e145fa40dc59ef33a1c8a42f013f85ba/pymongo-4.18.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d171e48ace55c33fda8598a046f3271da6c62aa3118b173770ecfdb846b8f207", size = 1117480, upload-time = "2026-09-24T16:59:35.593Z" }, - { url = "https://files.pythonhosted.org/packages/a4/2d/cb6aa46acaa0c3e615d535976458d0d535c481512dfcc2be58a3b4024417/pymongo-4.18.2-cp314-cp314t-win32.whl", hash = "sha256:a600edbe98e20e282d7331289429fff14d6426a062c8fc2234af6618a36d6ac3", size = 819030, upload-time = "2026-09-24T16:59:37.235Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ef/3ba4a20ebe5548b7749d472b00c7faf5e7e9824b6565e51dd76add046992/pymongo-4.18.2-cp314-cp314t-win_amd64.whl", hash = "sha256:1501f2dfd34cd435a2cd99e155d14fa53459c5f3b18ce86b0ff537d654ead6ea", size = 827700, upload-time = "2026-09-24T16:59:38.835Z" }, - { url = "https://files.pythonhosted.org/packages/1d/c7/d0340e5fe18e7351885924f9f20895c0bb23af5282052f97f35df6f4724f/pymongo-4.18.2-cp314-cp314t-win_arm64.whl", hash = "sha256:7beca033207a1ceb35f6f030d9d95bde7df09f1088312c932a64c29a6f0d7b88", size = 818008, upload-time = "2026-09-24T16:59:40.56Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/42/d8/2421a5ae0d6dcdaad2a0fb75d4071eaede9f764e73b829c62b6185c3ee6b/pymongo-4.18.3.tar.gz", hash = "sha256:5dd6e659b6014288a1c53458929402a58f44a032e6f29bcef44e7477c5268e48", size = 2747872, upload-time = "2026-10-08T19:44:08.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/73/1a73c32ba2d8852d82dbf604f157c3ac622b3d645280c6668e55baedc4e6/pymongo-4.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:555152e3be33d1ebaa6c47298ef2862f03c50af97bebeea1ff8c86c210098fb0", size = 819833, upload-time = "2026-10-08T19:41:50.927Z" }, + { url = "https://files.pythonhosted.org/packages/42/56/86fffa1bd7a289c4b83cf0f1faf6918440779f374b399d88e9f28baa5b39/pymongo-4.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f5eedd95a3470861f9dd02c6557665af8ac64d766fea58a51a9bcd4504c78308", size = 820287, upload-time = "2026-10-08T19:41:53.627Z" }, + { url = "https://files.pythonhosted.org/packages/51/26/f5425a1bf2d381794bb1e3fb78c2a72e825036ad3960b6e5088785a256f1/pymongo-4.18.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4a280957609056f77f2cd17a4c3bb42e6468055e74c8e3b79755b0db2986a0b7", size = 1041594, upload-time = "2026-10-08T19:41:55.676Z" }, + { url = "https://files.pythonhosted.org/packages/5a/89/eb0cb02908a281f91f597b3557536bf377df67bbb175ed5ada8317f5ee18/pymongo-4.18.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2261dd887f8e6b9e842f7871be3daebbe1dac222eee25a3e3ff6e0973425c66", size = 1050169, upload-time = "2026-10-08T19:41:57.245Z" }, + { url = "https://files.pythonhosted.org/packages/a5/c0/f39c56d2b71fa7106cbfdb48e500b88974688b2d12f6586401e6f472d402/pymongo-4.18.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b01a01f449d2923972ef38e9559d8289713aeb9ce8924159735dd76af2d23ee", size = 1074164, upload-time = "2026-10-08T19:41:58.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/52/598783aa611873306e9fd587c5fea569e0e4f541c25afa4d4a26ce22eb3e/pymongo-4.18.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6004f58612f56d7639213d08ab91162325d976ae17a82ecaafd33c9d644a1629", size = 1067395, upload-time = "2026-10-08T19:42:00.762Z" }, + { url = "https://files.pythonhosted.org/packages/4e/4f/023b7adc8090235394b0e9669fdb45e1a4917cee34868db43cb4bc692ec0/pymongo-4.18.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e540b3a8259f7c4bd6afb22253a639d1354c7b58ef49726d609abb2636cab4c3", size = 1052307, upload-time = "2026-10-08T19:42:02.427Z" }, + { url = "https://files.pythonhosted.org/packages/a2/b0/2cebb47c4cecee8a4a017ab5862366ef34f90ec5ff61d820cf0c324bc447/pymongo-4.18.3-cp310-cp310-win32.whl", hash = "sha256:114c57b7421e320d3fd5edcb3eebb4d2053978c8e5160b752cbdd81e2bf1a61b", size = 814643, upload-time = "2026-10-08T19:42:03.839Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d5/76beebc428f577e266bc5ad47a54bccc853d20b593a6a44e641ac3ba7b9d/pymongo-4.18.3-cp310-cp310-win_amd64.whl", hash = "sha256:f4860f9980c1c90bdf84081097381b7092623becdd2949d2afd2802e626b3326", size = 821860, upload-time = "2026-10-08T19:42:05.677Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f3/631d7ba2374e4f89363b4d270759d31d889bd5f29fa347878a67b81d1871/pymongo-4.18.3-cp310-cp310-win_arm64.whl", hash = "sha256:70b472e3477af60e870c6b7c513b029c2024a7e84e2e3892917b65bd06f53f73", size = 818145, upload-time = "2026-10-08T19:42:07.363Z" }, + { url = "https://files.pythonhosted.org/packages/58/a6/63bdeb527d98998b8ea2c667eca00d48f22dd42af281e9a2d7090632d54d/pymongo-4.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f00cb357d7cc7f2798116e2377732a409c43a6dc882f0241eafed7ffed50655", size = 818805, upload-time = "2026-10-08T19:42:09.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/e9/35602972d9fa98b894d1e5feef4db2f5d275298430e99b54b39f01125efa/pymongo-4.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fe2ef9c6eb6b75689e10b20a3d8119da87302481b0a7029f9399b35142adfd8", size = 819303, upload-time = "2026-10-08T19:42:10.749Z" }, + { url = "https://files.pythonhosted.org/packages/d8/e1/468f2c69b32565c93a56623535f248b091cf52e83c3509fa4b791427889f/pymongo-4.18.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ba6090d4bed582c97e38fa818c0a2b7443f203cb28882900b433ff713465f158", size = 1023754, upload-time = "2026-10-08T19:42:12.683Z" }, + { url = "https://files.pythonhosted.org/packages/e1/24/8af75e8af2427a47cfcc996444df934990ac4890d855f2ec064409d8b94b/pymongo-4.18.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f9903d0a089317422f52bbc25f5827e6656f0c42c43ed7d799bd02748e79a1", size = 1031920, upload-time = "2026-10-08T19:42:14.443Z" }, + { url = "https://files.pythonhosted.org/packages/78/d0/96fa79fb7cb47e6f09e58ccbe1a726330b5d052f395b65d57e286849ed43/pymongo-4.18.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ac9bf2304c2b092ccf04261ab0cddb7fd65df1cc1ae0fa57312b03396c00d28c", size = 1053859, upload-time = "2026-10-08T19:42:16.315Z" }, + { url = "https://files.pythonhosted.org/packages/4d/99/1b3f48bd3580c53e4a0e89bdc8cd8c15af94ca944f9de96582cb3ebfa5d0/pymongo-4.18.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5f37095428af3042f6bb1ebe269fedcbb645d9e0642b274e1cff026d3979500b", size = 1047418, upload-time = "2026-10-08T19:42:18.023Z" }, + { url = "https://files.pythonhosted.org/packages/08/1e/ab9148b15dcefd3d02852a65e4ac3a1df86248c227dd532dc53b41f33697/pymongo-4.18.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:16ade5053ab6c712fd25d3f878e38441b169d607d1326d708844a131911d029f", size = 1033358, upload-time = "2026-10-08T19:42:19.746Z" }, + { url = "https://files.pythonhosted.org/packages/39/93/bbbd0edfa33b10e7a86c864478dbb12163c946c142fd1e7b1bd0b6490353/pymongo-4.18.3-cp311-cp311-win32.whl", hash = "sha256:463c09e2cc208a65d35a1af3c613360cff6d58c8aef652273da07250bb214dba", size = 814425, upload-time = "2026-10-08T19:42:21.501Z" }, + { url = "https://files.pythonhosted.org/packages/1c/13/7515f91ed9e80968cc5dc9321ac97fafde07afb75123adb9e294f50c8a10/pymongo-4.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:1d7d0474012def6113c224b167aae661b926ac3b788219426830013ea25acd33", size = 821387, upload-time = "2026-10-08T19:42:23.349Z" }, + { url = "https://files.pythonhosted.org/packages/89/59/f54d5ee7d95ec4ed1f31bff014a0f61caa3e888d7a89a0585f3eb4be164b/pymongo-4.18.3-cp311-cp311-win_arm64.whl", hash = "sha256:83dff65baa6f2423857598ffc371d7412fa4d2a07c618bdc8d5053ade65de664", size = 817697, upload-time = "2026-10-08T19:42:25.128Z" }, + { url = "https://files.pythonhosted.org/packages/05/d5/4775a2891396ad125545e23b3024adae4bfac9553b70c924f1f372269dbf/pymongo-4.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ea78719dd05de3a919a52b94bec790c0d0cb7d07d2f7271711832664502a0782", size = 819374, upload-time = "2026-10-08T19:42:26.931Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0b/89ad56f43c3da6cbde100699f6b99528e78eba3c6740d8dad4ea2516aa45/pymongo-4.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6029d14761ba7243e6c5e464592013b519ad4dd3e4cfb75ddec39f4b5910711b", size = 819694, upload-time = "2026-10-08T19:42:28.76Z" }, + { url = "https://files.pythonhosted.org/packages/84/b4/b68ffc205441b0a6d36d6299e35e063a5d0d3264fd685428920e1f82b634/pymongo-4.18.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9536fb3820f721290f03ad07472ec2266d8f364f91de628679a7146c9c1dbe35", size = 1040136, upload-time = "2026-10-08T19:42:30.852Z" }, + { url = "https://files.pythonhosted.org/packages/c1/40/e779ff3d9165316c35a2f9742a42b9c3e3a678e9e2a9f6fe4128b7c551eb/pymongo-4.18.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e461bfca4861057929efa4215730b28b93b2adb4d07828d0b65475755bbf63f5", size = 1051430, upload-time = "2026-10-08T19:42:32.533Z" }, + { url = "https://files.pythonhosted.org/packages/07/9b/443ee038a739cc65a75f2078c9ef725c1cb4881545d2e9d7941c46f64a6c/pymongo-4.18.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1fef248623ed5e7406902a68d49dc0b1db434f19489f8d2fc9fe512c3c08bb1", size = 1075464, upload-time = "2026-10-08T19:42:34.325Z" }, + { url = "https://files.pythonhosted.org/packages/36/4b/d80518f675cd4c1215b770444bb83002454574dae0e69760af10703ed1e8/pymongo-4.18.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:213eaed8fc4f2b0f9c84323a229dea699e01e18b8fb39723f430123b6ee77813", size = 1069046, upload-time = "2026-10-08T19:42:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/e5/77/f2e9648c62e423c3b9dab1e16491a6c33250487c819e5c75784b35d16047/pymongo-4.18.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa6f363ff648bf061335d2190dd580cbf465b1308a7e6acb992d128d6a16a3bd", size = 1050321, upload-time = "2026-10-08T19:42:38.052Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e4/3236e3a87b29fc4c502ad7dd1521c20d4a29faf1db6fde6ae94d05c5ec27/pymongo-4.18.3-cp312-cp312-win32.whl", hash = "sha256:28ba8cae86ea02d7ffdf0eea81be69be80d35d6a4a3eba4dc436d3194341805a", size = 815300, upload-time = "2026-10-08T19:42:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/1e/18/3fa9d86ba32386c02ea991f10875a6a066dd5e5d80790243be3c141b0e73/pymongo-4.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:dc8ccf72b76c99a6b9fd05f8b89fe4a693128c5cfdba70f70e5792a6a563f6b0", size = 822079, upload-time = "2026-10-08T19:42:42.089Z" }, + { url = "https://files.pythonhosted.org/packages/03/50/65a7cefd3891b77994841992b2c5b59394667df64ef21377b8ac7ecdef47/pymongo-4.18.3-cp312-cp312-win_arm64.whl", hash = "sha256:4a1f7c7dc1d554449a1695d897eb42b6080a2f1e9ccd81385dfa00204979c54d", size = 817935, upload-time = "2026-10-08T19:42:43.98Z" }, + { url = "https://files.pythonhosted.org/packages/62/a4/225afd1d8d6e1df853b9aafe8f785304bb2e965b2f56c9ac4b61270aaf83/pymongo-4.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c5785fdb948a280140166ea24aac636e1f1de7142ff14ca23ddf9e2fd6b06916", size = 819273, upload-time = "2026-10-08T19:42:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/c2/6c/67d469f23654fa75ab6047b34fab232512e5688c75ce54e2c8e6248e9432/pymongo-4.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7cd8983db922f0c284b8ccb4182c5ecbc71831557f788bd6c46cbfafed853a6f", size = 819587, upload-time = "2026-10-08T19:42:48.128Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d6/be809af37976d329145d2496c847e430a76f66d51f6f10d2f54fbbba0d07/pymongo-4.18.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:185b3287bbe99fccf9571f2e5df5cd560ddc3cdc2c06852010346d040a8afb0f", size = 1040277, upload-time = "2026-10-08T19:42:50.296Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f4/79b1a8cc0163337f1b9728e31884db454ea615c47224b99ab0474007a861/pymongo-4.18.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f188904336022b84afa517cf2ee3cf9d3c42ab8ab107359e9bd4afd698d0cb0", size = 1051522, upload-time = "2026-10-08T19:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ca/600a7fdf1447a687a429df0f1ef6e112cef26b5e05f5bae502011c33d223/pymongo-4.18.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3c72fea937927b347efce39b63f604f2b7c6d975bc4fd1c7a916c82c96920ff1", size = 1075351, upload-time = "2026-10-08T19:42:54.178Z" }, + { url = "https://files.pythonhosted.org/packages/91/8e/6fa6e7e4d0fe9204fd4319d7ab3994356f497b475ecc8403a30a72f9240f/pymongo-4.18.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:710c0422c86e22b702f12f9b5e48d38309f264ca34eaed6c9ac163b0c697d01f", size = 1068853, upload-time = "2026-10-08T19:42:55.926Z" }, + { url = "https://files.pythonhosted.org/packages/31/3c/698ab3ae4d90d4547e6724f08c39db14432ca17f7fec5e7eafab3d54e818/pymongo-4.18.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f973cd934f9f943602418d4d0ff9a1371990741eaaeb7c6dbb421fec1345a828", size = 1050460, upload-time = "2026-10-08T19:42:57.786Z" }, + { url = "https://files.pythonhosted.org/packages/56/5b/4c2bec3a343cffffd6480bf6aefd0e413c3a9af3f6beedad4e79b8e7a855/pymongo-4.18.3-cp313-cp313-win32.whl", hash = "sha256:163cb12da5b5227d186bc420fbdb613f45f1525a8e48a5b8624894182a79fa29", size = 815327, upload-time = "2026-10-08T19:42:59.453Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5c/914d3eda4e321c67c87c32bfce1c1fb06ff62e61f33fa8b442273512742b/pymongo-4.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:6fed3281c93aafb79748c9448f32a1658a870499f09c0d70129f153c1a5833ef", size = 822114, upload-time = "2026-10-08T19:43:01.246Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cd/b315b2f2feb4394f24ed31399d96685936b9eb248b4016425e1ccb55f782/pymongo-4.18.3-cp313-cp313-win_arm64.whl", hash = "sha256:ff7585de6e5befc06eec004ac6352507685f901eac92ea0c79ae5defae374a96", size = 817974, upload-time = "2026-10-08T19:43:03.318Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f9/7037282744f7fe86d4a86c8745ea0ec8f8e644ecc63f3b600f1af56fb225/pymongo-4.18.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a7c8471eca11f8ec2ae3a4315f44a2f6edcd0e144573d7bf003907eb8096883f", size = 819169, upload-time = "2026-10-08T19:43:05.201Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/4d5fa6e9d5b068cad6a608d0dffffcc61b357e7d3e6950c4c70b93d9f72c/pymongo-4.18.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d2b1b531d212dd375a2ddc59d421d09f8a6bc5782fb688e4a65ff0d89e7bf0ad", size = 819677, upload-time = "2026-10-08T19:43:07.275Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bc/eccb6237d4c1c7cfd5f91ed4e4131f033b02170fcfcaa2d85a918c54ca86/pymongo-4.18.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2edaaff5cc7b2cb0cc216a01d85a413476abdf3cd7be5fc4025506be6434d2cc", size = 1042064, upload-time = "2026-10-08T19:43:09.461Z" }, + { url = "https://files.pythonhosted.org/packages/8d/71/e822fc1c0dd80b3ab25a90af070776568fa5441ea41559a001255b4d78ca/pymongo-4.18.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b19fc2f492263561bab174bc97dc59a70a164a1cac02620b47a13b575310c128", size = 1051775, upload-time = "2026-10-08T19:43:11.425Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a3/7aafbbaac6b8815a84b24a7ea68ae569c041dae55c9b49407c02be446090/pymongo-4.18.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99de1deaa55b17d0f8a2ceafd7908baaafa08151e2d0d668fdc03d0f607f5d33", size = 1075303, upload-time = "2026-10-08T19:43:13.374Z" }, + { url = "https://files.pythonhosted.org/packages/e4/02/f4326578ad9c7c2bebea6ef849afc31878dd946fbb5724dbfa8c479fc607/pymongo-4.18.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c90575489ebe2ee8c0b4009efd7d4143037113092f6b28fb66e8f8ea0ca60c71", size = 1065783, upload-time = "2026-10-08T19:43:15.34Z" }, + { url = "https://files.pythonhosted.org/packages/26/ec/eecd7abf22839c42abbcd09293be922d46227c07857c726d738797c30950/pymongo-4.18.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75c038d39e23b38b968fd7c61060c8611859c51e411d52f7b97be49bf8bf0d10", size = 1050123, upload-time = "2026-10-08T19:43:17.206Z" }, + { url = "https://files.pythonhosted.org/packages/c2/98/765449cd031e2763541fc144fcc6af8df0a5021355214c44ab4d9d78787b/pymongo-4.18.3-cp314-cp314-win32.whl", hash = "sha256:01da84a43a37b5ab327dbe7cf9f2612f9963c4ca093390d2211671eb996b26cc", size = 816497, upload-time = "2026-10-08T19:43:19.066Z" }, + { url = "https://files.pythonhosted.org/packages/fb/53/a432246287fa2ead90546c855b9ad62c0fd2fa783f9042f1d762d7d18ef0/pymongo-4.18.3-cp314-cp314-win_amd64.whl", hash = "sha256:82f620a555a646f2218cfbf6c39b722e4cbfc71bd9fee019af5e72cbbe7488f7", size = 823661, upload-time = "2026-10-08T19:43:20.895Z" }, + { url = "https://files.pythonhosted.org/packages/d9/63/8b725508ac9f438730c35ca701e1db18e7332e5cf0ef905729419c11dbc8/pymongo-4.18.3-cp314-cp314-win_arm64.whl", hash = "sha256:a8677a3f7127144f4a100a62ef264f9143a986aa1acd3aa35a0d027fd2aafec1", size = 819257, upload-time = "2026-10-08T19:43:22.912Z" }, + { url = "https://files.pythonhosted.org/packages/30/30/bc0b397d0b87399fa2ce20cc14b54198073cc5bee5821a84fe8b5478945a/pymongo-4.18.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f502830b94acd44f252f305be2e71c6f067acb690970f6910be50e1c7d6d217", size = 822168, upload-time = "2026-10-08T19:43:24.943Z" }, + { url = "https://files.pythonhosted.org/packages/87/62/4212628f536db4c630c082f27747346642acf58d27a3206c7c9d2edf6bed/pymongo-4.18.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a5bcfaa3ea009c73afabfaaf8bfd6f3b61f32eaaf68e85660f3337724acc0f62", size = 822524, upload-time = "2026-10-08T19:43:27.011Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f1/abe1519ce3b5fe125cd6b246dd998ea1989feb456427821558d59f449c63/pymongo-4.18.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4159ab20e5784b2e2b783bc80a4bbda52cfd19ddede5a4a80327ffb7d260db8c", size = 1105944, upload-time = "2026-10-08T19:43:28.998Z" }, + { url = "https://files.pythonhosted.org/packages/e2/36/5ee745e7e61a5f63437a16a4f8b8f6fe7cd5d1fd9ae2ce6ef48e607c8219/pymongo-4.18.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ca11bf9d64d7b7827350cd8bd4ae96ddd38669a3ce04860118994061c5fbdd6", size = 1125679, upload-time = "2026-10-08T19:43:31.269Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ad/89d37b9a79c73a5c8f3e6ab82ee440dbc3e82e12c53aa8b424ec1c4cc5ae/pymongo-4.18.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e443366af09655938a7614c6ca1566ccd94f7042ce470c4a67dfe2179cec2f9", size = 1144903, upload-time = "2026-10-08T19:43:33.28Z" }, + { url = "https://files.pythonhosted.org/packages/8e/2c/17bb29e9c4b46d479523a15efef9b736a561c52b855ec8afbf20191c4027/pymongo-4.18.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:05838fcc42c277d6293ca3e85d5c959beaa355f515b877ef56a048bb1c6660ae", size = 1136719, upload-time = "2026-10-08T19:43:35.507Z" }, + { url = "https://files.pythonhosted.org/packages/b5/be/d6e6bb72a7e4b800ceacac092c399bcb1336362ac54a721637e2bde46cdc/pymongo-4.18.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7efcf4ef53c8a49e438a646ee838f927d4e05acd872a09b54aa97c07fb2059c1", size = 1118144, upload-time = "2026-10-08T19:43:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/64/61/bbb877abbb6ee8222648ef284b9936d4c164d64530a702e009d15c9dfe11/pymongo-4.18.3-cp314-cp314t-win32.whl", hash = "sha256:89df07473db610b6aa1c7a3ac9bcc80dd50b088f85c00657435895216230c071", size = 819185, upload-time = "2026-10-08T19:43:40.188Z" }, + { url = "https://files.pythonhosted.org/packages/cc/e9/dead464714489d234f03ec007ba57b83c2ae4fa8b82e71bb83c689409ddc/pymongo-4.18.3-cp314-cp314t-win_amd64.whl", hash = "sha256:25d43632506dc98598ac1e45018ae18cb88137035df954bac04b5a700417521f", size = 827906, upload-time = "2026-10-08T19:43:42.451Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4a/1f2a5230bda2a1a3fb94457bceb9ea3919be40666da32fddb4d64e9a7fd6/pymongo-4.18.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4214355fae9e12f99c288662720123002944ba7fa186ea62f431e37842380c4f", size = 820082, upload-time = "2026-10-08T19:43:44.459Z" }, ] [[package]] @@ -4107,45 +4164,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.16.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/78/449cb84790bd5cc3823b2652ee405a4558856e5c4195aee3a16bf7b3eb5d/ruff-0.16.8.tar.gz", hash = "sha256:9247bf92b5f04d825c8639a4fe423ec2e4222acd9222e58412b0dab7e442798b", size = 4938814, upload-time = "2026-09-16T15:54:46.688Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ac/25/6071aabc530e9be7e2c195e8fe3f7aea2735405b6cf447212832d7811831/ruff-0.16.8-py3-none-linux_armv6l.whl", hash = "sha256:6ffbd6d87383c1edf5f6fa890f10200950240d7c1a16052a19a09d3a2307dd38", size = 10048966, upload-time = "2026-09-16T15:53:57.605Z" }, - { url = "https://files.pythonhosted.org/packages/54/98/07f90ecbc74dd5fb5764f11f2bc774d6a7cffef92d2ff5f5b4e9e23c754e/ruff-0.16.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:42ed6b878ed61e3acca92f2730a17acff39286944ea82398544696366a6f925e", size = 10165498, upload-time = "2026-09-16T15:54:01.14Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1f/e6a712e3b47cad4a40600134105ed193cb773f618a42eb7ba323cb812cc0/ruff-0.16.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ea781c7f2afba8c6a505ea0fb3f994020249e0c450635f5381286fea6b46170", size = 9830004, upload-time = "2026-09-16T15:54:03.998Z" }, - { url = "https://files.pythonhosted.org/packages/23/f2/311a08776d75d81c7676e20b6b020ae63cbe881fcdc7a8dd64e6e18bdd93/ruff-0.16.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8efeae3bbe414a5efefda11a792dfb51ef90ac48d50c4830de2f644caf3e8659", size = 9986558, upload-time = "2026-09-16T15:54:06.804Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ed/37b6cb3d3ba8c73e68ae3eb1d502383beb5aa05a582bb7bb3a922f929f54/ruff-0.16.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a79b795469fef7fc6e908b218eed2eb17332afd85031db6480dc864560e69b2", size = 9877332, upload-time = "2026-09-16T15:54:09.552Z" }, - { url = "https://files.pythonhosted.org/packages/22/cc/40873a8f36ad084cc540d55fcca7077264d5b13b24659e9180c176fb2b08/ruff-0.16.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fdc5563cdc50555e6fba39322850860e9267c1b3d12c26a74729d8604c3c812", size = 10507125, upload-time = "2026-09-16T15:54:12.152Z" }, - { url = "https://files.pythonhosted.org/packages/c3/e4/fc91a642b78ccbab6b9477720f3644ae7a10a9bcce69a934679cd64f62bc/ruff-0.16.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34508983c70665578dab88f5223d8e6228307e1135398ca8bfc8b7e9501e282b", size = 11336694, upload-time = "2026-09-16T15:54:15.489Z" }, - { url = "https://files.pythonhosted.org/packages/c2/3d/bbd2a9a600a4e73dc3e7548a249c8d1671273464b55822c6fae50f602dff/ruff-0.16.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644bb578569e0ffc575741232bd385dacdd6fbe123f1a729e7a225f54aa3957f", size = 10774448, upload-time = "2026-09-16T15:54:18.16Z" }, - { url = "https://files.pythonhosted.org/packages/1a/41/d83af9879a7b6e8bf5fe16b1da0b134049d2f5d3afac12defb0897cb84bd/ruff-0.16.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15e7d226246961db9235098333caa13063906d3851136b84c2900b82f5daa1df", size = 10323796, upload-time = "2026-09-16T15:54:20.743Z" }, - { url = "https://files.pythonhosted.org/packages/f5/2c/cefd07bfe914b84943ea769ade8d607bd22750b965d3228eefd7cebd15d0/ruff-0.16.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2bf6bc3e9ebdd4449abc6f06cf64b98051a2c61cf94d2fe9596518c881f1a1e", size = 10514115, upload-time = "2026-09-16T15:54:23.497Z" }, - { url = "https://files.pythonhosted.org/packages/f3/9d/76a2e26c79a23be6e6e3664c57bec9e9fc8de155cfb9e4b67ea91b64f9d7/ruff-0.16.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ca111ba0849539165e9e59d2b442542f3c1e8060ebbdea82494f1ffbccb1e1f", size = 10072582, upload-time = "2026-09-16T15:54:26.185Z" }, - { url = "https://files.pythonhosted.org/packages/2e/d4/f42edddb39668af1a559ceafa3823aedd65633a48dc9768e775485faa2c1/ruff-0.16.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:359a1e5b495448ee1e91018064382ebc86f90e8aac2fed222c7d0e4e8df85fd2", size = 9879644, upload-time = "2026-09-16T15:54:29.278Z" }, - { url = "https://files.pythonhosted.org/packages/f8/d4/913e3195d95e0378786c6656945c865f534a3560e29139da4882aff630d1/ruff-0.16.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:59e8f5681349474110b24d62e93cfda6593f5fa3473446ca3705200cac1a08b9", size = 10231569, upload-time = "2026-09-16T15:54:32.036Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c4/8aa6ea0bdcedbd1bf87397e2fc4ed8406448ea5842f8660bc6e5f163039d/ruff-0.16.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:efa3e7a16d1baaa79957888dfdf8be9ef2e44db81cb032af06d76632ab59e773", size = 10663666, upload-time = "2026-09-16T15:54:34.838Z" }, - { url = "https://files.pythonhosted.org/packages/3d/02/7f10ef4700bc223c30a3fdd10631a29830c45524b810a3c7ed947af64591/ruff-0.16.8-py3-none-win32.whl", hash = "sha256:55793ba85c69921e89be061426d91a78652d6e50317c962240922747a4eb713f", size = 10093472, upload-time = "2026-09-16T15:54:37.47Z" }, - { url = "https://files.pythonhosted.org/packages/1e/5d/a509c07d714b6da88f2c518b4637cf6f1d46b074be8f0f1e5fb9ff5126fe/ruff-0.16.8-py3-none-win_amd64.whl", hash = "sha256:a6b85621fd3c81e31fc5f5add09c9c078b430db3595ca632efafdec9e64ebfaa", size = 10586899, upload-time = "2026-09-16T15:54:40.488Z" }, - { url = "https://files.pythonhosted.org/packages/fe/a0/50787329e4f20bf9dc9f6230015d46ec69c51a97ace5bc202dae4755365d/ruff-0.16.8-py3-none-win_arm64.whl", hash = "sha256:d075e820af612102ce217f07cc93e69f9490b10ec13ea85fa87bd03d996cef8a", size = 10386316, upload-time = "2026-09-16T15:54:43.332Z" }, -] - -[[package]] -name = "runloop-api-client" -version = "1.32.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "httpx", extra = ["http2"] }, - { name = "pydantic" }, - { name = "sniffio" }, - { name = "typing-extensions" }, - { name = "uuid-utils" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2b/d8/e85232182d50fa8374f495bb518e29b0d7de8aacfc86e4598019eb8718e9/runloop_api_client-1.32.0.tar.gz", hash = "sha256:1f2077fb5a99b5ef30dbc1d96d47c1ca1d3f77895b9e186d985e47365fe85634", size = 670118, upload-time = "2026-09-08T23:00:33.741Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/17/7c9bd35638526ab83d2a40e67454b3445d5de5a74a0e56e52541edefa3a5/runloop_api_client-1.32.0-py3-none-any.whl", hash = "sha256:23c36910de7423f73aef91d4a963abea80a6b2f5b2df5bb8e439d788d5486cc7", size = 414368, upload-time = "2026-09-08T23:00:35.085Z" }, +version = "0.17.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e7/e6d5749983b020e6cb1055abec0a6f361a8e94f0184cdcf56d4760995f02/ruff-0.17.1.tar.gz", hash = "sha256:5bb796c5112e9fb9527f2ef2faa0a130d6312d19de92bcfaddd70dbb2d41020f", size = 5323349, upload-time = "2026-10-15T15:23:18.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/78/90c46ca6250709f7c37e397b60d06061adee8d0f7d13959ed6a8e49b14fd/ruff-0.17.1-py3-none-linux_armv6l.whl", hash = "sha256:1796c41eafec66f4635125dd1e074a7aa27b8d20c187d484360acf11d247fc4a", size = 10147528, upload-time = "2026-10-15T15:22:33.667Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d2/a2e28bc35b083ae51fa07dfba5280a667c8b01ecaee7802fc8849dcd5c46/ruff-0.17.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8eadb09a788010be29c895418bdfa513e4e1c65e64b94e5b19e4cd381f6cc7fb", size = 10225974, upload-time = "2026-10-15T15:22:36.328Z" }, + { url = "https://files.pythonhosted.org/packages/8e/71/7a9029a81c8206ee01a98f6cfeeb56e6c095af86857581a37045c46d7e94/ruff-0.17.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:29b454f18ca3eeaf37fbf140841dcf27d36109e29dee9aae8acecb68c26c6f74", size = 9270361, upload-time = "2026-10-15T15:22:38.651Z" }, + { url = "https://files.pythonhosted.org/packages/c0/8c/e075e9c917cb0575297880c96768304e881240171c870cbd8d9b7b54bec2/ruff-0.17.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:872e03dd231744d6c5ec5c8af6be52b71323fc6ee096e4c8b34d9f844ee3b978", size = 9938300, upload-time = "2026-10-15T15:22:41.284Z" }, + { url = "https://files.pythonhosted.org/packages/87/27/0b4bb926a49de2383f161272b4d863571a4c7ecdbf595a6bc58813a13367/ruff-0.17.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4c0d6cb5f8fbac77eec9af49f4fc0eaf856b05545e372b0694d24b4e76b1c640", size = 9948464, upload-time = "2026-10-15T15:22:43.681Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/518d57e0e02aecb8ea4acac6b57b51a53173d25fa734952bfdd7d531e88e/ruff-0.17.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78beced4a3c94dfedbee4b999b95b6a27b35820fdc0afbacc6d37a8da26c1cd1", size = 10638639, upload-time = "2026-10-15T15:22:46.081Z" }, + { url = "https://files.pythonhosted.org/packages/01/82/d89a4e498ade59e6cee52d67f8da569019372212b87468c1fdeae884601c/ruff-0.17.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7feefc63921202894f0e158b0e32f45a5950d70a7fb693ccb9264816c25b1c05", size = 11337238, upload-time = "2026-10-15T15:22:48.894Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7c/3b0fdef9352656317c977086fe1d802902406e4f2a0d8578da90c0e6d1c5/ruff-0.17.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684154861a819bf1f433669d2a0ce9c3e54677c65b8ebb866cc2a7ec86345785", size = 10896327, upload-time = "2026-10-15T15:22:51.45Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/5566290219708ce2928f3a29c4f843a1185b341ec6111c4631a33c5ceced/ruff-0.17.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6ef98e51de8ef38082f90653ceeb84944bd7673783eea13e1647c5b9b064189", size = 10350811, upload-time = "2026-10-15T15:22:54.401Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d2/ca8462dabb3c413990d49703ba306c30c862195a75448d080fa8fcce9775/ruff-0.17.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8f9aa5b5bd5fa1937803ea609c8f153b8de39bf3e0e149897ae34b0c5b52f467", size = 10569994, upload-time = "2026-10-15T15:22:57.142Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ad/dd1864c1f1db17c8cfb297ce70b352388e6977620f66f7182c52855407df/ruff-0.17.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:80e53b0e5faa3c5667f035daaa032893ae42b7cfe708c886d39ed131e54688ab", size = 10059672, upload-time = "2026-10-15T15:23:00.084Z" }, + { url = "https://files.pythonhosted.org/packages/65/45/3f56cf1d2168c2165cdd5ac6f9668aee66a4cfdd4caf2b2cc5ccb2b1d568/ruff-0.17.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:056c5d5fa0ac790051ae165d7035c7bf956d02de2db557f2e9b31d877a505fca", size = 9947803, upload-time = "2026-10-15T15:23:03.132Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4b/5457cb2be4c1d1ea5e919558131a0f197a4a454a0530d9e30820e8206e4c/ruff-0.17.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bd8f5a55b10f371642eacd0e8665a42a8b97da15c9a7187c79e697b77b5e0ce4", size = 10351807, upload-time = "2026-10-15T15:23:05.628Z" }, + { url = "https://files.pythonhosted.org/packages/49/fb/db15b4f8fa24a1e36b99e1401f42ef6df16ee299a48670d5d262b771e7d7/ruff-0.17.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ce05e6f3973611d9550a600e04bb531061e368baba31610eb57c1509a8f21a0a", size = 10765716, upload-time = "2026-10-15T15:23:08.092Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e8/1e186578076aff7a80735f625005fde678412ab609cd323ed182eb7d6b87/ruff-0.17.1-py3-none-win32.whl", hash = "sha256:1de0030a8aa78c65c5fecb84b2e5c390ac684c0452875a02afc7c083d49a496a", size = 9895093, upload-time = "2026-10-15T15:23:10.582Z" }, + { url = "https://files.pythonhosted.org/packages/6a/2e/da3d9589bae8e8b58f3a48bd5bc0b980232fb14a83bf02c3be7660f87c1b/ruff-0.17.1-py3-none-win_amd64.whl", hash = "sha256:5df76f16580fab58d0f6c98a3ac254a4abceb7b2489b8b678335869cff77b3d7", size = 10097955, upload-time = "2026-10-15T15:23:13.309Z" }, + { url = "https://files.pythonhosted.org/packages/df/c9/0f2a4dea06a5e12c1191ee3d96b1e1233797d0d5e18f97a2b909a95a9836/ruff-0.17.1-py3-none-win_arm64.whl", hash = "sha256:42400b9a6ff8515ff27796f07f92e0195be011c86e81e381606f58ea5502fef4", size = 9853542, upload-time = "2026-10-15T15:23:16.05Z" }, ] [[package]] @@ -4577,35 +4616,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/92/9d/c4e665119135114480843e7ab388fa94d8480650450e6f8e26b70d323a4c/urllib3-2.8.0-py3-none-any.whl", hash = "sha256:0cf3cae568d36aa9576b28dfb35f11328f1cb974ca7647d9475ebb86c75ac6e3", size = 135717, upload-time = "2026-09-15T19:29:34.577Z" }, ] -[[package]] -name = "uuid-utils" -version = "0.14.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/d1/38a573f0c631c062cf42fa1f5d021d4dd3c31fb23e4376e4b56b0c9fbbed/uuid_utils-0.14.1.tar.gz", hash = "sha256:9bfc95f64af80ccf129c604fb6b8ca66c6f256451e32bc4570f760e4309c9b69", size = 22195, upload-time = "2026-02-20T22:50:38.833Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/b7/add4363039a34506a58457d96d4aa2126061df3a143eb4d042aedd6a2e76/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:93a3b5dc798a54a1feb693f2d1cb4cf08258c32ff05ae4929b5f0a2ca624a4f0", size = 604679, upload-time = "2026-02-20T22:50:27.469Z" }, - { url = "https://files.pythonhosted.org/packages/dd/84/d1d0bef50d9e66d31b2019997c741b42274d53dde2e001b7a83e9511c339/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ccd65a4b8e83af23eae5e56d88034b2fe7264f465d3e830845f10d1591b81741", size = 309346, upload-time = "2026-02-20T22:50:31.857Z" }, - { url = "https://files.pythonhosted.org/packages/ef/ed/b6d6fd52a6636d7c3eddf97d68da50910bf17cd5ac221992506fb56cf12e/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b56b0cacd81583834820588378e432b0696186683b813058b707aedc1e16c4b1", size = 344714, upload-time = "2026-02-20T22:50:42.642Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a7/a19a1719fb626fe0b31882db36056d44fe904dc0cf15b06fdf56b2679cf7/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb3cf14de789097320a3c56bfdfdd51b1225d11d67298afbedee7e84e3837c96", size = 350914, upload-time = "2026-02-20T22:50:36.487Z" }, - { url = "https://files.pythonhosted.org/packages/1d/fc/f6690e667fdc3bb1a73f57951f97497771c56fe23e3d302d7404be394d4f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e0854a90d67f4b0cc6e54773deb8be618f4c9bad98d3326f081423b5d14fae", size = 482609, upload-time = "2026-02-20T22:50:37.511Z" }, - { url = "https://files.pythonhosted.org/packages/54/6e/dcd3fa031320921a12ec7b4672dea3bd1dd90ddffa363a91831ba834d559/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6743ba194de3910b5feb1a62590cd2587e33a73ab6af8a01b642ceb5055862", size = 345699, upload-time = "2026-02-20T22:50:46.87Z" }, - { url = "https://files.pythonhosted.org/packages/04/28/e5220204b58b44ac0047226a9d016a113fde039280cc8732d9e6da43b39f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:043fb58fde6cf1620a6c066382f04f87a8e74feb0f95a585e4ed46f5d44af57b", size = 372205, upload-time = "2026-02-20T22:50:28.438Z" }, - { url = "https://files.pythonhosted.org/packages/c7/d9/3d2eb98af94b8dfffc82b6a33b4dfc87b0a5de2c68a28f6dde0db1f8681b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c915d53f22945e55fe0d3d3b0b87fd965a57f5fd15666fd92d6593a73b1dd297", size = 521836, upload-time = "2026-02-20T22:50:23.057Z" }, - { url = "https://files.pythonhosted.org/packages/a8/15/0eb106cc6fe182f7577bc0ab6e2f0a40be247f35c5e297dbf7bbc460bd02/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0972488e3f9b449e83f006ead5a0e0a33ad4a13e4462e865b7c286ab7d7566a3", size = 625260, upload-time = "2026-02-20T22:50:25.949Z" }, - { url = "https://files.pythonhosted.org/packages/3c/17/f539507091334b109e7496830af2f093d9fc8082411eafd3ece58af1f8ba/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:1c238812ae0c8ffe77d8d447a32c6dfd058ea4631246b08b5a71df586ff08531", size = 587824, upload-time = "2026-02-20T22:50:35.225Z" }, - { url = "https://files.pythonhosted.org/packages/2e/c2/d37a7b2e41f153519367d4db01f0526e0d4b06f1a4a87f1c5dfca5d70a8b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bec8f8ef627af86abf8298e7ec50926627e29b34fa907fcfbedb45aaa72bca43", size = 551407, upload-time = "2026-02-20T22:50:44.915Z" }, - { url = "https://files.pythonhosted.org/packages/65/36/2d24b2cbe78547c6532da33fb8613debd3126eccc33a6374ab788f5e46e9/uuid_utils-0.14.1-cp39-abi3-win32.whl", hash = "sha256:b54d6aa6252d96bac1fdbc80d26ba71bad9f220b2724d692ad2f2310c22ef523", size = 183476, upload-time = "2026-02-20T22:50:32.745Z" }, - { url = "https://files.pythonhosted.org/packages/83/92/2d7e90df8b1a69ec4cff33243ce02b7a62f926ef9e2f0eca5a026889cd73/uuid_utils-0.14.1-cp39-abi3-win_amd64.whl", hash = "sha256:fc27638c2ce267a0ce3e06828aff786f91367f093c80625ee21dad0208e0f5ba", size = 187147, upload-time = "2026-02-20T22:50:45.807Z" }, - { url = "https://files.pythonhosted.org/packages/d9/26/529f4beee17e5248e37e0bc17a2761d34c0fa3b1e5729c88adb2065bae6e/uuid_utils-0.14.1-cp39-abi3-win_arm64.whl", hash = "sha256:b04cb49b42afbc4ff8dbc60cf054930afc479d6f4dd7f1ec3bbe5dbfdde06b7a", size = 188132, upload-time = "2026-02-20T22:50:41.718Z" }, - { url = "https://files.pythonhosted.org/packages/91/f9/6c64bdbf71f58ccde7919e00491812556f446a5291573af92c49a5e9aaef/uuid_utils-0.14.1-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b197cd5424cf89fb019ca7f53641d05bfe34b1879614bed111c9c313b5574cd8", size = 591617, upload-time = "2026-02-20T22:50:24.532Z" }, - { url = "https://files.pythonhosted.org/packages/d0/f0/758c3b0fb0c4871c7704fef26a5bc861de4f8a68e4831669883bebe07b0f/uuid_utils-0.14.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:12c65020ba6cb6abe1d57fcbfc2d0ea0506c67049ee031714057f5caf0f9bc9c", size = 303702, upload-time = "2026-02-20T22:50:40.687Z" }, - { url = "https://files.pythonhosted.org/packages/85/89/d91862b544c695cd58855efe3201f83894ed82fffe34500774238ab8eba7/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b5d2ad28063d422ccc2c28d46471d47b61a58de885d35113a8f18cb547e25bf", size = 337678, upload-time = "2026-02-20T22:50:39.768Z" }, - { url = "https://files.pythonhosted.org/packages/ee/6b/cf342ba8a898f1de024be0243fac67c025cad530c79ea7f89c4ce718891a/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da2234387b45fde40b0fedfee64a0ba591caeea9c48c7698ab6e2d85c7991533", size = 343711, upload-time = "2026-02-20T22:50:43.965Z" }, - { url = "https://files.pythonhosted.org/packages/b3/20/049418d094d396dfa6606b30af925cc68a6670c3b9103b23e6990f84b589/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50fffc2827348c1e48972eed3d1c698959e63f9d030aa5dd82ba451113158a62", size = 476731, upload-time = "2026-02-20T22:50:30.589Z" }, - { url = "https://files.pythonhosted.org/packages/77/a1/0857f64d53a90321e6a46a3d4cc394f50e1366132dcd2ae147f9326ca98b/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dbe718765f70f5b7f9b7f66b6a937802941b1cc56bcf642ce0274169741e01", size = 338902, upload-time = "2026-02-20T22:50:33.927Z" }, - { url = "https://files.pythonhosted.org/packages/ed/d0/5bf7cbf1ac138c92b9ac21066d18faf4d7e7f651047b700eb192ca4b9fdb/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:258186964039a8e36db10810c1ece879d229b01331e09e9030bc5dcabe231bd2", size = 364700, upload-time = "2026-02-20T22:50:21.732Z" }, -] - [[package]] name = "uvicorn" version = "0.35.0" @@ -4962,101 +4972,152 @@ wheels = [ [[package]] name = "yarl" -version = "1.20.1" +version = "1.25.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3c/fb/efaa23fa4e45537b827620f04cf8f3cd658b76642205162e072703a5b963/yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac", size = 186428, upload-time = "2025-06-10T00:46:09.923Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/65/7fed0d774abf47487c64be14e9223749468922817b5e8792b8a64792a1bb/yarl-1.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6032e6da6abd41e4acda34d75a816012717000fa6839f37124a47fcefc49bec4", size = 132910, upload-time = "2025-06-10T00:42:31.108Z" }, - { url = "https://files.pythonhosted.org/packages/8a/7b/988f55a52da99df9e56dc733b8e4e5a6ae2090081dc2754fc8fd34e60aa0/yarl-1.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2c7b34d804b8cf9b214f05015c4fee2ebe7ed05cf581e7192c06555c71f4446a", size = 90644, upload-time = "2025-06-10T00:42:33.851Z" }, - { url = "https://files.pythonhosted.org/packages/f7/de/30d98f03e95d30c7e3cc093759982d038c8833ec2451001d45ef4854edc1/yarl-1.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c869f2651cc77465f6cd01d938d91a11d9ea5d798738c1dc077f3de0b5e5fed", size = 89322, upload-time = "2025-06-10T00:42:35.688Z" }, - { url = "https://files.pythonhosted.org/packages/e0/7a/f2f314f5ebfe9200724b0b748de2186b927acb334cf964fd312eb86fc286/yarl-1.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62915e6688eb4d180d93840cda4110995ad50c459bf931b8b3775b37c264af1e", size = 323786, upload-time = "2025-06-10T00:42:37.817Z" }, - { url = "https://files.pythonhosted.org/packages/15/3f/718d26f189db96d993d14b984ce91de52e76309d0fd1d4296f34039856aa/yarl-1.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:41ebd28167bc6af8abb97fec1a399f412eec5fd61a3ccbe2305a18b84fb4ca73", size = 319627, upload-time = "2025-06-10T00:42:39.937Z" }, - { url = "https://files.pythonhosted.org/packages/a5/76/8fcfbf5fa2369157b9898962a4a7d96764b287b085b5b3d9ffae69cdefd1/yarl-1.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21242b4288a6d56f04ea193adde174b7e347ac46ce6bc84989ff7c1b1ecea84e", size = 339149, upload-time = "2025-06-10T00:42:42.627Z" }, - { url = "https://files.pythonhosted.org/packages/3c/95/d7fc301cc4661785967acc04f54a4a42d5124905e27db27bb578aac49b5c/yarl-1.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bea21cdae6c7eb02ba02a475f37463abfe0a01f5d7200121b03e605d6a0439f8", size = 333327, upload-time = "2025-06-10T00:42:44.842Z" }, - { url = "https://files.pythonhosted.org/packages/65/94/e21269718349582eee81efc5c1c08ee71c816bfc1585b77d0ec3f58089eb/yarl-1.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f8a891e4a22a89f5dde7862994485e19db246b70bb288d3ce73a34422e55b23", size = 326054, upload-time = "2025-06-10T00:42:47.149Z" }, - { url = "https://files.pythonhosted.org/packages/32/ae/8616d1f07853704523519f6131d21f092e567c5af93de7e3e94b38d7f065/yarl-1.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd803820d44c8853a109a34e3660e5a61beae12970da479cf44aa2954019bf70", size = 315035, upload-time = "2025-06-10T00:42:48.852Z" }, - { url = "https://files.pythonhosted.org/packages/48/aa/0ace06280861ef055855333707db5e49c6e3a08840a7ce62682259d0a6c0/yarl-1.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b982fa7f74c80d5c0c7b5b38f908971e513380a10fecea528091405f519b9ebb", size = 338962, upload-time = "2025-06-10T00:42:51.024Z" }, - { url = "https://files.pythonhosted.org/packages/20/52/1e9d0e6916f45a8fb50e6844f01cb34692455f1acd548606cbda8134cd1e/yarl-1.20.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:33f29ecfe0330c570d997bcf1afd304377f2e48f61447f37e846a6058a4d33b2", size = 335399, upload-time = "2025-06-10T00:42:53.007Z" }, - { url = "https://files.pythonhosted.org/packages/f2/65/60452df742952c630e82f394cd409de10610481d9043aa14c61bf846b7b1/yarl-1.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:835ab2cfc74d5eb4a6a528c57f05688099da41cf4957cf08cad38647e4a83b30", size = 338649, upload-time = "2025-06-10T00:42:54.964Z" }, - { url = "https://files.pythonhosted.org/packages/7b/f5/6cd4ff38dcde57a70f23719a838665ee17079640c77087404c3d34da6727/yarl-1.20.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:46b5e0ccf1943a9a6e766b2c2b8c732c55b34e28be57d8daa2b3c1d1d4009309", size = 358563, upload-time = "2025-06-10T00:42:57.28Z" }, - { url = "https://files.pythonhosted.org/packages/d1/90/c42eefd79d0d8222cb3227bdd51b640c0c1d0aa33fe4cc86c36eccba77d3/yarl-1.20.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:df47c55f7d74127d1b11251fe6397d84afdde0d53b90bedb46a23c0e534f9d24", size = 357609, upload-time = "2025-06-10T00:42:59.055Z" }, - { url = "https://files.pythonhosted.org/packages/03/c8/cea6b232cb4617514232e0f8a718153a95b5d82b5290711b201545825532/yarl-1.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76d12524d05841276b0e22573f28d5fbcb67589836772ae9244d90dd7d66aa13", size = 350224, upload-time = "2025-06-10T00:43:01.248Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a3/eaa0ab9712f1f3d01faf43cf6f1f7210ce4ea4a7e9b28b489a2261ca8db9/yarl-1.20.1-cp310-cp310-win32.whl", hash = "sha256:6c4fbf6b02d70e512d7ade4b1f998f237137f1417ab07ec06358ea04f69134f8", size = 81753, upload-time = "2025-06-10T00:43:03.486Z" }, - { url = "https://files.pythonhosted.org/packages/8f/34/e4abde70a9256465fe31c88ed02c3f8502b7b5dead693a4f350a06413f28/yarl-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:aef6c4d69554d44b7f9d923245f8ad9a707d971e6209d51279196d8e8fe1ae16", size = 86817, upload-time = "2025-06-10T00:43:05.231Z" }, - { url = "https://files.pythonhosted.org/packages/b1/18/893b50efc2350e47a874c5c2d67e55a0ea5df91186b2a6f5ac52eff887cd/yarl-1.20.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47ee6188fea634bdfaeb2cc420f5b3b17332e6225ce88149a17c413c77ff269e", size = 133833, upload-time = "2025-06-10T00:43:07.393Z" }, - { url = "https://files.pythonhosted.org/packages/89/ed/b8773448030e6fc47fa797f099ab9eab151a43a25717f9ac043844ad5ea3/yarl-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0f6500f69e8402d513e5eedb77a4e1818691e8f45e6b687147963514d84b44b", size = 91070, upload-time = "2025-06-10T00:43:09.538Z" }, - { url = "https://files.pythonhosted.org/packages/e3/e3/409bd17b1e42619bf69f60e4f031ce1ccb29bd7380117a55529e76933464/yarl-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a8900a42fcdaad568de58887c7b2f602962356908eedb7628eaf6021a6e435b", size = 89818, upload-time = "2025-06-10T00:43:11.575Z" }, - { url = "https://files.pythonhosted.org/packages/f8/77/64d8431a4d77c856eb2d82aa3de2ad6741365245a29b3a9543cd598ed8c5/yarl-1.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bad6d131fda8ef508b36be3ece16d0902e80b88ea7200f030a0f6c11d9e508d4", size = 347003, upload-time = "2025-06-10T00:43:14.088Z" }, - { url = "https://files.pythonhosted.org/packages/8d/d2/0c7e4def093dcef0bd9fa22d4d24b023788b0a33b8d0088b51aa51e21e99/yarl-1.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:df018d92fe22aaebb679a7f89fe0c0f368ec497e3dda6cb81a567610f04501f1", size = 336537, upload-time = "2025-06-10T00:43:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/f0/f3/fc514f4b2cf02cb59d10cbfe228691d25929ce8f72a38db07d3febc3f706/yarl-1.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f969afbb0a9b63c18d0feecf0db09d164b7a44a053e78a7d05f5df163e43833", size = 362358, upload-time = "2025-06-10T00:43:18.704Z" }, - { url = "https://files.pythonhosted.org/packages/ea/6d/a313ac8d8391381ff9006ac05f1d4331cee3b1efaa833a53d12253733255/yarl-1.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:812303eb4aa98e302886ccda58d6b099e3576b1b9276161469c25803a8db277d", size = 357362, upload-time = "2025-06-10T00:43:20.888Z" }, - { url = "https://files.pythonhosted.org/packages/00/70/8f78a95d6935a70263d46caa3dd18e1f223cf2f2ff2037baa01a22bc5b22/yarl-1.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98c4a7d166635147924aa0bf9bfe8d8abad6fffa6102de9c99ea04a1376f91e8", size = 348979, upload-time = "2025-06-10T00:43:23.169Z" }, - { url = "https://files.pythonhosted.org/packages/cb/05/42773027968968f4f15143553970ee36ead27038d627f457cc44bbbeecf3/yarl-1.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12e768f966538e81e6e7550f9086a6236b16e26cd964cf4df35349970f3551cf", size = 337274, upload-time = "2025-06-10T00:43:27.111Z" }, - { url = "https://files.pythonhosted.org/packages/05/be/665634aa196954156741ea591d2f946f1b78ceee8bb8f28488bf28c0dd62/yarl-1.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe41919b9d899661c5c28a8b4b0acf704510b88f27f0934ac7a7bebdd8938d5e", size = 363294, upload-time = "2025-06-10T00:43:28.96Z" }, - { url = "https://files.pythonhosted.org/packages/eb/90/73448401d36fa4e210ece5579895731f190d5119c4b66b43b52182e88cd5/yarl-1.20.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8601bc010d1d7780592f3fc1bdc6c72e2b6466ea34569778422943e1a1f3c389", size = 358169, upload-time = "2025-06-10T00:43:30.701Z" }, - { url = "https://files.pythonhosted.org/packages/c3/b0/fce922d46dc1eb43c811f1889f7daa6001b27a4005587e94878570300881/yarl-1.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:daadbdc1f2a9033a2399c42646fbd46da7992e868a5fe9513860122d7fe7a73f", size = 362776, upload-time = "2025-06-10T00:43:32.51Z" }, - { url = "https://files.pythonhosted.org/packages/f1/0d/b172628fce039dae8977fd22caeff3eeebffd52e86060413f5673767c427/yarl-1.20.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:03aa1e041727cb438ca762628109ef1333498b122e4c76dd858d186a37cec845", size = 381341, upload-time = "2025-06-10T00:43:34.543Z" }, - { url = "https://files.pythonhosted.org/packages/6b/9b/5b886d7671f4580209e855974fe1cecec409aa4a89ea58b8f0560dc529b1/yarl-1.20.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:642980ef5e0fa1de5fa96d905c7e00cb2c47cb468bfcac5a18c58e27dbf8d8d1", size = 379988, upload-time = "2025-06-10T00:43:36.489Z" }, - { url = "https://files.pythonhosted.org/packages/73/be/75ef5fd0fcd8f083a5d13f78fd3f009528132a1f2a1d7c925c39fa20aa79/yarl-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:86971e2795584fe8c002356d3b97ef6c61862720eeff03db2a7c86b678d85b3e", size = 371113, upload-time = "2025-06-10T00:43:38.592Z" }, - { url = "https://files.pythonhosted.org/packages/50/4f/62faab3b479dfdcb741fe9e3f0323e2a7d5cd1ab2edc73221d57ad4834b2/yarl-1.20.1-cp311-cp311-win32.whl", hash = "sha256:597f40615b8d25812f14562699e287f0dcc035d25eb74da72cae043bb884d773", size = 81485, upload-time = "2025-06-10T00:43:41.038Z" }, - { url = "https://files.pythonhosted.org/packages/f0/09/d9c7942f8f05c32ec72cd5c8e041c8b29b5807328b68b4801ff2511d4d5e/yarl-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:26ef53a9e726e61e9cd1cda6b478f17e350fb5800b4bd1cd9fe81c4d91cfeb2e", size = 86686, upload-time = "2025-06-10T00:43:42.692Z" }, - { url = "https://files.pythonhosted.org/packages/5f/9a/cb7fad7d73c69f296eda6815e4a2c7ed53fc70c2f136479a91c8e5fbdb6d/yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9", size = 133667, upload-time = "2025-06-10T00:43:44.369Z" }, - { url = "https://files.pythonhosted.org/packages/67/38/688577a1cb1e656e3971fb66a3492501c5a5df56d99722e57c98249e5b8a/yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a", size = 91025, upload-time = "2025-06-10T00:43:46.295Z" }, - { url = "https://files.pythonhosted.org/packages/50/ec/72991ae51febeb11a42813fc259f0d4c8e0507f2b74b5514618d8b640365/yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2", size = 89709, upload-time = "2025-06-10T00:43:48.22Z" }, - { url = "https://files.pythonhosted.org/packages/99/da/4d798025490e89426e9f976702e5f9482005c548c579bdae792a4c37769e/yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee", size = 352287, upload-time = "2025-06-10T00:43:49.924Z" }, - { url = "https://files.pythonhosted.org/packages/1a/26/54a15c6a567aac1c61b18aa0f4b8aa2e285a52d547d1be8bf48abe2b3991/yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819", size = 345429, upload-time = "2025-06-10T00:43:51.7Z" }, - { url = "https://files.pythonhosted.org/packages/d6/95/9dcf2386cb875b234353b93ec43e40219e14900e046bf6ac118f94b1e353/yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16", size = 365429, upload-time = "2025-06-10T00:43:53.494Z" }, - { url = "https://files.pythonhosted.org/packages/91/b2/33a8750f6a4bc224242a635f5f2cff6d6ad5ba651f6edcccf721992c21a0/yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6", size = 363862, upload-time = "2025-06-10T00:43:55.766Z" }, - { url = "https://files.pythonhosted.org/packages/98/28/3ab7acc5b51f4434b181b0cee8f1f4b77a65919700a355fb3617f9488874/yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd", size = 355616, upload-time = "2025-06-10T00:43:58.056Z" }, - { url = "https://files.pythonhosted.org/packages/36/a3/f666894aa947a371724ec7cd2e5daa78ee8a777b21509b4252dd7bd15e29/yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a", size = 339954, upload-time = "2025-06-10T00:43:59.773Z" }, - { url = "https://files.pythonhosted.org/packages/f1/81/5f466427e09773c04219d3450d7a1256138a010b6c9f0af2d48565e9ad13/yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38", size = 365575, upload-time = "2025-06-10T00:44:02.051Z" }, - { url = "https://files.pythonhosted.org/packages/2e/e3/e4b0ad8403e97e6c9972dd587388940a032f030ebec196ab81a3b8e94d31/yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef", size = 365061, upload-time = "2025-06-10T00:44:04.196Z" }, - { url = "https://files.pythonhosted.org/packages/ac/99/b8a142e79eb86c926f9f06452eb13ecb1bb5713bd01dc0038faf5452e544/yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f", size = 364142, upload-time = "2025-06-10T00:44:06.527Z" }, - { url = "https://files.pythonhosted.org/packages/34/f2/08ed34a4a506d82a1a3e5bab99ccd930a040f9b6449e9fd050320e45845c/yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8", size = 381894, upload-time = "2025-06-10T00:44:08.379Z" }, - { url = "https://files.pythonhosted.org/packages/92/f8/9a3fbf0968eac704f681726eff595dce9b49c8a25cd92bf83df209668285/yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a", size = 383378, upload-time = "2025-06-10T00:44:10.51Z" }, - { url = "https://files.pythonhosted.org/packages/af/85/9363f77bdfa1e4d690957cd39d192c4cacd1c58965df0470a4905253b54f/yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004", size = 374069, upload-time = "2025-06-10T00:44:12.834Z" }, - { url = "https://files.pythonhosted.org/packages/35/99/9918c8739ba271dcd935400cff8b32e3cd319eaf02fcd023d5dcd487a7c8/yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5", size = 81249, upload-time = "2025-06-10T00:44:14.731Z" }, - { url = "https://files.pythonhosted.org/packages/eb/83/5d9092950565481b413b31a23e75dd3418ff0a277d6e0abf3729d4d1ce25/yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698", size = 86710, upload-time = "2025-06-10T00:44:16.716Z" }, - { url = "https://files.pythonhosted.org/packages/8a/e1/2411b6d7f769a07687acee88a062af5833cf1966b7266f3d8dfb3d3dc7d3/yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a", size = 131811, upload-time = "2025-06-10T00:44:18.933Z" }, - { url = "https://files.pythonhosted.org/packages/b2/27/584394e1cb76fb771371770eccad35de400e7b434ce3142c2dd27392c968/yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3", size = 90078, upload-time = "2025-06-10T00:44:20.635Z" }, - { url = "https://files.pythonhosted.org/packages/bf/9a/3246ae92d4049099f52d9b0fe3486e3b500e29b7ea872d0f152966fc209d/yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7", size = 88748, upload-time = "2025-06-10T00:44:22.34Z" }, - { url = "https://files.pythonhosted.org/packages/a3/25/35afe384e31115a1a801fbcf84012d7a066d89035befae7c5d4284df1e03/yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691", size = 349595, upload-time = "2025-06-10T00:44:24.314Z" }, - { url = "https://files.pythonhosted.org/packages/28/2d/8aca6cb2cabc8f12efcb82749b9cefecbccfc7b0384e56cd71058ccee433/yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31", size = 342616, upload-time = "2025-06-10T00:44:26.167Z" }, - { url = "https://files.pythonhosted.org/packages/0b/e9/1312633d16b31acf0098d30440ca855e3492d66623dafb8e25b03d00c3da/yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28", size = 361324, upload-time = "2025-06-10T00:44:27.915Z" }, - { url = "https://files.pythonhosted.org/packages/bc/a0/688cc99463f12f7669eec7c8acc71ef56a1521b99eab7cd3abb75af887b0/yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653", size = 359676, upload-time = "2025-06-10T00:44:30.041Z" }, - { url = "https://files.pythonhosted.org/packages/af/44/46407d7f7a56e9a85a4c207724c9f2c545c060380718eea9088f222ba697/yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5", size = 352614, upload-time = "2025-06-10T00:44:32.171Z" }, - { url = "https://files.pythonhosted.org/packages/b1/91/31163295e82b8d5485d31d9cf7754d973d41915cadce070491778d9c9825/yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02", size = 336766, upload-time = "2025-06-10T00:44:34.494Z" }, - { url = "https://files.pythonhosted.org/packages/b4/8e/c41a5bc482121f51c083c4c2bcd16b9e01e1cf8729e380273a952513a21f/yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53", size = 364615, upload-time = "2025-06-10T00:44:36.856Z" }, - { url = "https://files.pythonhosted.org/packages/e3/5b/61a3b054238d33d70ea06ebba7e58597891b71c699e247df35cc984ab393/yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc", size = 360982, upload-time = "2025-06-10T00:44:39.141Z" }, - { url = "https://files.pythonhosted.org/packages/df/a3/6a72fb83f8d478cb201d14927bc8040af901811a88e0ff2da7842dd0ed19/yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04", size = 369792, upload-time = "2025-06-10T00:44:40.934Z" }, - { url = "https://files.pythonhosted.org/packages/7c/af/4cc3c36dfc7c077f8dedb561eb21f69e1e9f2456b91b593882b0b18c19dc/yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4", size = 382049, upload-time = "2025-06-10T00:44:42.854Z" }, - { url = "https://files.pythonhosted.org/packages/19/3a/e54e2c4752160115183a66dc9ee75a153f81f3ab2ba4bf79c3c53b33de34/yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b", size = 384774, upload-time = "2025-06-10T00:44:45.275Z" }, - { url = "https://files.pythonhosted.org/packages/9c/20/200ae86dabfca89060ec6447649f219b4cbd94531e425e50d57e5f5ac330/yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1", size = 374252, upload-time = "2025-06-10T00:44:47.31Z" }, - { url = "https://files.pythonhosted.org/packages/83/75/11ee332f2f516b3d094e89448da73d557687f7d137d5a0f48c40ff211487/yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7", size = 81198, upload-time = "2025-06-10T00:44:49.164Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ba/39b1ecbf51620b40ab402b0fc817f0ff750f6d92712b44689c2c215be89d/yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c", size = 86346, upload-time = "2025-06-10T00:44:51.182Z" }, - { url = "https://files.pythonhosted.org/packages/43/c7/669c52519dca4c95153c8ad96dd123c79f354a376346b198f438e56ffeb4/yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d", size = 138826, upload-time = "2025-06-10T00:44:52.883Z" }, - { url = "https://files.pythonhosted.org/packages/6a/42/fc0053719b44f6ad04a75d7f05e0e9674d45ef62f2d9ad2c1163e5c05827/yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf", size = 93217, upload-time = "2025-06-10T00:44:54.658Z" }, - { url = "https://files.pythonhosted.org/packages/4f/7f/fa59c4c27e2a076bba0d959386e26eba77eb52ea4a0aac48e3515c186b4c/yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3", size = 92700, upload-time = "2025-06-10T00:44:56.784Z" }, - { url = "https://files.pythonhosted.org/packages/2f/d4/062b2f48e7c93481e88eff97a6312dca15ea200e959f23e96d8ab898c5b8/yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d", size = 347644, upload-time = "2025-06-10T00:44:59.071Z" }, - { url = "https://files.pythonhosted.org/packages/89/47/78b7f40d13c8f62b499cc702fdf69e090455518ae544c00a3bf4afc9fc77/yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c", size = 323452, upload-time = "2025-06-10T00:45:01.605Z" }, - { url = "https://files.pythonhosted.org/packages/eb/2b/490d3b2dc66f52987d4ee0d3090a147ea67732ce6b4d61e362c1846d0d32/yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1", size = 346378, upload-time = "2025-06-10T00:45:03.946Z" }, - { url = "https://files.pythonhosted.org/packages/66/ad/775da9c8a94ce925d1537f939a4f17d782efef1f973039d821cbe4bcc211/yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce", size = 353261, upload-time = "2025-06-10T00:45:05.992Z" }, - { url = "https://files.pythonhosted.org/packages/4b/23/0ed0922b47a4f5c6eb9065d5ff1e459747226ddce5c6a4c111e728c9f701/yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3", size = 335987, upload-time = "2025-06-10T00:45:08.227Z" }, - { url = "https://files.pythonhosted.org/packages/3e/49/bc728a7fe7d0e9336e2b78f0958a2d6b288ba89f25a1762407a222bf53c3/yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be", size = 329361, upload-time = "2025-06-10T00:45:10.11Z" }, - { url = "https://files.pythonhosted.org/packages/93/8f/b811b9d1f617c83c907e7082a76e2b92b655400e61730cd61a1f67178393/yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16", size = 346460, upload-time = "2025-06-10T00:45:12.055Z" }, - { url = "https://files.pythonhosted.org/packages/70/fd/af94f04f275f95da2c3b8b5e1d49e3e79f1ed8b6ceb0f1664cbd902773ff/yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513", size = 334486, upload-time = "2025-06-10T00:45:13.995Z" }, - { url = "https://files.pythonhosted.org/packages/84/65/04c62e82704e7dd0a9b3f61dbaa8447f8507655fd16c51da0637b39b2910/yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f", size = 342219, upload-time = "2025-06-10T00:45:16.479Z" }, - { url = "https://files.pythonhosted.org/packages/91/95/459ca62eb958381b342d94ab9a4b6aec1ddec1f7057c487e926f03c06d30/yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390", size = 350693, upload-time = "2025-06-10T00:45:18.399Z" }, - { url = "https://files.pythonhosted.org/packages/a6/00/d393e82dd955ad20617abc546a8f1aee40534d599ff555ea053d0ec9bf03/yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458", size = 355803, upload-time = "2025-06-10T00:45:20.677Z" }, - { url = "https://files.pythonhosted.org/packages/9e/ed/c5fb04869b99b717985e244fd93029c7a8e8febdfcffa06093e32d7d44e7/yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e", size = 341709, upload-time = "2025-06-10T00:45:23.221Z" }, - { url = "https://files.pythonhosted.org/packages/24/fd/725b8e73ac2a50e78a4534ac43c6addf5c1c2d65380dd48a9169cc6739a9/yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d", size = 86591, upload-time = "2025-06-10T00:45:25.793Z" }, - { url = "https://files.pythonhosted.org/packages/94/c3/b2e9f38bc3e11191981d57ea08cab2166e74ea770024a646617c9cddd9f6/yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f", size = 93003, upload-time = "2025-06-10T00:45:27.752Z" }, - { url = "https://files.pythonhosted.org/packages/b4/2d/2345fce04cfd4bee161bf1e7d9cdc702e3e16109021035dbb24db654a622/yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77", size = 46542, upload-time = "2025-06-10T00:46:07.521Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/75/16/e8be8e2fb175bbf41a0680381a319f1199fae256588241a2ac8677eafb49/yarl-1.25.1.tar.gz", hash = "sha256:03dd38de09bc213e9a8b29761eec33ee1d5318dac0e49d8af36e4d27830e23a7", size = 246245, upload-time = "2026-09-15T19:35:02.264Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/92/55fa9ee84cb8ec9930a910b5753936a926f918d8cc8965bdac571479c095/yarl-1.25.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:142c06c4d6a35ee3ec5da08499805e879cb3ca7c1fbfbecb0140fe72403818d6", size = 144695, upload-time = "2026-09-15T19:29:53.114Z" }, + { url = "https://files.pythonhosted.org/packages/16/b4/9edc8e605b16b2eb5bd0c2ccc73e2b15aab583862460767fb43f91f11839/yarl-1.25.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:24ce942011a61953e7d313438038f4d32ff21387b775f58a957f7a07dd55ef95", size = 104332, upload-time = "2026-09-15T19:29:55.443Z" }, + { url = "https://files.pythonhosted.org/packages/ae/de/4204e6646e278b1cd4a9cf73ebe8b7cfcf16693f1249b324eb273d67602a/yarl-1.25.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e23c82b63cd7652fc24d33ed6cc17099d607aa3b4fc4ddc75e95062f3d82df4", size = 104449, upload-time = "2026-09-15T19:29:57.239Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fc/21d74789198ad68a23a209ab0d73fe5a52d39ebbd9945041e72827acdd77/yarl-1.25.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ee202350cf57abf0e9502a41601841019c25d3db7ff52d980aaf31446254059", size = 116828, upload-time = "2026-09-15T19:29:59.031Z" }, + { url = "https://files.pythonhosted.org/packages/8f/31/90891ddb61848c067ebd1fb505ab5902a6e7d4707c2f3823f67ff37883d3/yarl-1.25.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5df89f769cc8ff94c3d7e7603386fba309d25ce5240132d26c15baa8d0e96c4c", size = 107150, upload-time = "2026-09-15T19:30:00.868Z" }, + { url = "https://files.pythonhosted.org/packages/a1/15/f3d18743d3688b5f01aa83034b6c00665692ab20e1d0c89a29000d4979ea/yarl-1.25.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:83e9f4a25085bd4b7214701a0794ff1f50fc633ffb8bdfebf07abdd81c2db126", size = 124704, upload-time = "2026-09-15T19:30:03.128Z" }, + { url = "https://files.pythonhosted.org/packages/99/37/718789d8004775d6a2db86b2afc72b1d156e0590b88a1b8634ddd4bdb8ee/yarl-1.25.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e636b64d24fd9c38053c5e389a1174c66361fa49dcfd220f4dd35b4abde7cb89", size = 129246, upload-time = "2026-09-15T19:30:05.014Z" }, + { url = "https://files.pythonhosted.org/packages/28/b9/7818ac6dec7fbcd16be2d19495807c01e2c38834e57d41c63356a75e786b/yarl-1.25.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e5637ca8d0bd7fb72648a6c7934af4baaccb697657f7438c9d264fc2abb8b0b1", size = 118071, upload-time = "2026-09-15T19:30:06.977Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e3/8d098cafb30a64df7283b5a5dd0d32d22a71b18a1b9ec071d0776ffe5c2a/yarl-1.25.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:683e362b8ba453080f7489c66f4ea794e751c35b72e7eab3575ef784c2fbc7fb", size = 116180, upload-time = "2026-09-15T19:30:08.836Z" }, + { url = "https://files.pythonhosted.org/packages/20/9c/fbe9432478d87e00eef80242e15179639df162cb3d5d8d978fc56dcb6c51/yarl-1.25.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:df23df54b5114a17c2d0ef192433e2e5a9f0c5178c32375e90b7cfc965f349d0", size = 116578, upload-time = "2026-09-15T19:30:10.782Z" }, + { url = "https://files.pythonhosted.org/packages/b1/8e/b3dfd03732236b86b0bcf06a72a37de412ff6f82e719a947c3c651d4dece/yarl-1.25.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f53dcd26694f148f738edc052b5a69234833e739f10f4c3287bdfd8ec0f7b326", size = 108886, upload-time = "2026-09-15T19:30:12.951Z" }, + { url = "https://files.pythonhosted.org/packages/83/e1/94dacb650d4963d5f844bc3df7ba70288cb7b68ed5a4d070da71acb19b14/yarl-1.25.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b8075fe90bc08e40b8b8a1874fab42ee4c7b56af05c5886e9cc841397f916908", size = 124098, upload-time = "2026-09-15T19:30:14.963Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3f/c93f76a218258c7bfc60dd27fd56e9102bdbc625517f4a6ee673295fe4e4/yarl-1.25.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a8c2b841478068440d8b733005d13a5ef535b9928cbc05f17182d410f32ba449", size = 115768, upload-time = "2026-09-15T19:30:16.736Z" }, + { url = "https://files.pythonhosted.org/packages/a8/79/4d93f13c3b05cda3c962805dec28cbc255c50239b3457808abc5633a00c2/yarl-1.25.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ca32926d7d77bcc8838425c4c95e040a3ace1cb7dfdae599013458dcda2607ca", size = 122461, upload-time = "2026-09-15T19:30:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8e/e2ba83b3a9bfc1d3b882ad35fa8abc04a7af4d9356f2e329a23d0c8d889d/yarl-1.25.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:192a866877a49993949ef1975864ad8728bea28ee810f6abe1a0729c2b500426", size = 118295, upload-time = "2026-09-15T19:30:21.07Z" }, + { url = "https://files.pythonhosted.org/packages/42/67/cb5ea1baa0c0ac60bda44ce59f601133154af2a2e67722d3517f8793855d/yarl-1.25.1-cp310-cp310-win_amd64.whl", hash = "sha256:3f4d48a6112712973e676bd792121fee470e432d749177162d9949d5c9460a1b", size = 102929, upload-time = "2026-09-15T19:30:23.122Z" }, + { url = "https://files.pythonhosted.org/packages/2f/85/9a1e98de10fc0e738d517efd2a9984c3e7db6cf35fa72b2b373988a9e9b7/yarl-1.25.1-cp310-cp310-win_arm64.whl", hash = "sha256:48796ea00a303961507dc6c8437c4b325a6fc3f95f7c36c71b91ea9a8150963c", size = 98854, upload-time = "2026-09-15T19:30:25.102Z" }, + { url = "https://files.pythonhosted.org/packages/83/b3/2cea721d495ca57f8f414aa4867ee263f486b274e07463158cf52f02ba7f/yarl-1.25.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9d693bf4bf534e9ba3ae2780cfd577f5135629f7b5ac653490859d0b77864865", size = 143794, upload-time = "2026-09-15T19:30:26.946Z" }, + { url = "https://files.pythonhosted.org/packages/55/e6/cd145cff8e5cf60b8b3c41fbecfa2a45028a8dec3fbc52bec03595ff3d3b/yarl-1.25.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ab2054c5531af2a9ba7b69b8ec91e4f884420e83a8c5e579b013084cb57e5e5d", size = 103993, upload-time = "2026-09-15T19:30:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/ce/7c/fbb40fe2d53747c40aa36a9e2bd2178a202f942bda0b670f3306d4aefbce/yarl-1.25.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:564fdc7085d2245ab84f88882fdb1d6ac0723124bff6ded35bfb1c00f812630d", size = 104010, upload-time = "2026-09-15T19:30:31.069Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0b/5a516f70641092283f57cf3670bdb75e7327bcc0dcb5038697e4dfbfd569/yarl-1.25.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acae6b45d1ace09b6ba3876da43b88366ef368f73b988c7f57e14231753d4420", size = 116444, upload-time = "2026-09-15T19:30:32.894Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/d1a627f827b0a404ae0f5647cab0534959081cde13b0756a783469fb3b5e/yarl-1.25.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1fb2a01ba8cd9c5d2c5dc1ec35e0fc951d04b4f037541d4ac090c993ce58b3d7", size = 107565, upload-time = "2026-09-15T19:30:35.188Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cf/c8e0aaec886840a6c4480fe44eaec7cd4319f79a563b79321473be79c56f/yarl-1.25.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e92b6bcc741b86d67606c40d3cb9c7cc8e6c737f81e31f4a94efc204456c92e3", size = 125006, upload-time = "2026-09-15T19:30:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/50/26/0cce366d54a93cdc8342965dc7663385e4db161625cc1e8b18e786753d24/yarl-1.25.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:72c34ac7ad4314c19362d5ce27626dcc8429bd30bbf8c179f4234078851f9492", size = 128717, upload-time = "2026-09-15T19:30:38.904Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/a71501bbc1a674ff72c4d6c2b75f4d9a5af819f5244c3a7558080a8802c5/yarl-1.25.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5add7b4ca7afeea91d52e4d4e4db3b1fe9885b71f07054560d8c4296b7441a2", size = 117728, upload-time = "2026-09-15T19:30:40.809Z" }, + { url = "https://files.pythonhosted.org/packages/e7/6f/c3267ca01defeed9ed9c4ff9b17bd54915432c405945233265b707475d1c/yarl-1.25.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:def538065f9e4d4cf1ae164bd59aba00dfa84f03923e0de4c3788f252d6bcd17", size = 116224, upload-time = "2026-09-15T19:30:42.818Z" }, + { url = "https://files.pythonhosted.org/packages/8f/69/fad57ee52d648431718ee0f1f68966a99c1352c3924688ffbdcc9d3fe51a/yarl-1.25.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a191bfdb30a79b98e5d175d75285f9fcb78bf0e46ba5efda042e1c72071a0de", size = 116299, upload-time = "2026-09-15T19:30:44.966Z" }, + { url = "https://files.pythonhosted.org/packages/2a/d4/6a8c1e29f33338687ca278cba0a8fbf6525a322c2c02a9a500ccbe041152/yarl-1.25.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:71f42c5b9a948c113bbdebfa544598321431d064ff959d32e99b1feb61d68345", size = 108625, upload-time = "2026-09-15T19:30:46.874Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d2/3a35ae791c9cb6522c106923ff25c3230d999091e5e65511bca23bbd9914/yarl-1.25.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:72849d892954be4d09e569b8b831ac39ce58417fedc767d4308a0fe542018a40", size = 124515, upload-time = "2026-09-15T19:30:49.082Z" }, + { url = "https://files.pythonhosted.org/packages/be/fd/2b022109a6b4af0f7dc371cf7500af380b0d4f034010243e1b0ce218dc93/yarl-1.25.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:efb01a106f971cb3752856bca2318bbdf7f01bd8823779c461586cbe5ffd5258", size = 115711, upload-time = "2026-09-15T19:30:51.208Z" }, + { url = "https://files.pythonhosted.org/packages/18/59/f7586271136c3ddb0126bbfe661844699369b76b555fe38c4efe86870b2e/yarl-1.25.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a1daf47cd95a7c3a63456336bc5aaa8c86dd3a47d07ed3d0e76132ae4666a5a1", size = 122751, upload-time = "2026-09-15T19:30:53.535Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0b/07f7a2d881f7e16c385b47fc1753382600847cad305dcc7fa0c25828acf8/yarl-1.25.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9489e6abf47ba37f332075a91444c7cfedb03e6ce99fbb2f116bfe1ce810da3b", size = 117983, upload-time = "2026-09-15T19:30:55.277Z" }, + { url = "https://files.pythonhosted.org/packages/aa/9d/8cdceec66a9b940700cb45931741403f045b162afd79bcb93c41cadd0972/yarl-1.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7306dee25b8a0e737363f347362b875094b4dc4e367311470656ae420fdbf8e", size = 102894, upload-time = "2026-09-15T19:30:57.606Z" }, + { url = "https://files.pythonhosted.org/packages/17/f1/7ec357db1d3ad2863542d71e8fe64a126bbec17b134cd7d30951196809a5/yarl-1.25.1-cp311-cp311-win_arm64.whl", hash = "sha256:abb1384477f5901d436b5d2e5465954de46ea6098f59163d243660b5c4461d35", size = 98609, upload-time = "2026-09-15T19:30:59.705Z" }, + { url = "https://files.pythonhosted.org/packages/75/b3/cd32ac66ae622b854c2df0ac52106dda220d361b65a64fde7d5b3684aa3f/yarl-1.25.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:94d7aa6debf92a1dd14cb5280b083a764169a13cfb23a452111160274ed989f4", size = 144798, upload-time = "2026-09-15T19:31:01.821Z" }, + { url = "https://files.pythonhosted.org/packages/61/fb/a2c52a8007c2051ba74662afb112ecf3d00346af4c25e33df9d80fd14fb8/yarl-1.25.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:83d4a37e4b95da4d8bda930d6d35b75b4cdadbacbb4980cae290ea3100b5d51d", size = 104583, upload-time = "2026-09-15T19:31:04.05Z" }, + { url = "https://files.pythonhosted.org/packages/be/dd/ee38aec8e09fdf957e50d4085453fbe202f56c6c3b4cf07b81cdb4f09ee9/yarl-1.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e029648f9c951db30e98a7d7ec90835db88ec4b32820efe2a9bdc2287e032eb6", size = 104325, upload-time = "2026-09-15T19:31:06.338Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b3/058dbfb1857b484c9cf9cc135659f50b85ce66e03c99e44dc2f7b6161f55/yarl-1.25.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d781294bb815ecb5ea57ff6bbf8038e0a31a95fdf3e1788f66e0dc100d64b58", size = 115358, upload-time = "2026-09-15T19:31:08.593Z" }, + { url = "https://files.pythonhosted.org/packages/db/39/29693446cf0cf6b15a0e2f75a5d40f93c56819b05b0622196f45e95b5cc0/yarl-1.25.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e12c538e00e7c1b286a07061046b90e8124e6a9793efae2c70db6a4aad07faad", size = 107658, upload-time = "2026-09-15T19:31:10.802Z" }, + { url = "https://files.pythonhosted.org/packages/86/b3/3c4dd7e1af43b931fba95e0a722737f2ea94a6d199c802585282831d7abd/yarl-1.25.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e4de3ac4adbad3d0bc7c6f4360a7dbff5de2f15e3b723be3198074e17fd9c40", size = 122660, upload-time = "2026-09-15T19:31:12.84Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b5/1b60dbc3cfc9c5712b15148c206748f2bc93953ffdbe25ea75b63dfc89c9/yarl-1.25.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:419f392a1da624877975709e3864dfe833af6cc7671b39318086d456e288380c", size = 126506, upload-time = "2026-09-15T19:31:15.088Z" }, + { url = "https://files.pythonhosted.org/packages/bc/7b/ca212cbe170ac8b96e45317ecbcf9c3c3ecf0cdec98d5b088a9c4088929b/yarl-1.25.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6f117789d22dce188e5754e8bc65b7e6ebf8cb73963b9fa761f672a5883769d", size = 117050, upload-time = "2026-09-15T19:31:17.241Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c3/72b4938cdbe619ad71ac156182faef4908846b84dc3ca4dbb4c4e6f84014/yarl-1.25.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80e47012e730da131c9f059c80936783f9659aae22dc31c03c0595590d11ed54", size = 114174, upload-time = "2026-09-15T19:31:19.294Z" }, + { url = "https://files.pythonhosted.org/packages/e8/43/268717870f9ba0cc9701a95181587f6dc8c5f387aab4aeecc83158f38a79/yarl-1.25.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e80f557716fd765439577131e526b8942ffc2c07bdbc5e39fa62f660ba1e963f", size = 114944, upload-time = "2026-09-15T19:31:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/da/84/baa5bf504d51fe062c4bcaf62936da97fffb43285978d0b39984824231fd/yarl-1.25.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f61964f235a43738bfac50da46fc4254943a7eea3051aeb0b6fc7c992c29fadc", size = 108263, upload-time = "2026-09-15T19:31:23.388Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/779a2ed9e0152a601a27039bed9aead3f0b79797a67e2c44bfa444622dd8/yarl-1.25.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e546fe1d4a93ebc2910f0d768baff19faa09843ab3f2036a67ed6e69fae4419d", size = 122184, upload-time = "2026-09-15T19:31:25.343Z" }, + { url = "https://files.pythonhosted.org/packages/f8/1f/118e9e5b8f07694d63fd3222e801d7782270003f1a222aa798df3f8d5933/yarl-1.25.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cce0727fd5ac04d372fa9bbfde9febc2bcf209aadfcf0468e45dec72719895d1", size = 114001, upload-time = "2026-09-15T19:31:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/a4cf1cf372313734b17996d4007f9f73596e7a178b9485802e5494ecf484/yarl-1.25.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af4ea5b37403ef4e30f3927eaed540db942bde01d8d3ff083527c0704d1c9c68", size = 120565, upload-time = "2026-09-15T19:31:29.47Z" }, + { url = "https://files.pythonhosted.org/packages/05/79/ad94f93ca731bc9e44d321833ab96b82a4f9f5f63cf773f81a4aeea5ecc1/yarl-1.25.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:68782fdb4027b8d1eee25ec35e9a6db05e863b899eb0310b3a33b6c3fef55707", size = 117060, upload-time = "2026-09-15T19:31:31.367Z" }, + { url = "https://files.pythonhosted.org/packages/bb/cc/51a7b4abf4ac593b8e7eb3794b28e5a35ae26eed8bc04787628d215af82f/yarl-1.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:7d575b54cb3863ef9bc290ea4b009999d55dc237326131e4853cf33e888fee03", size = 102593, upload-time = "2026-09-15T19:31:33.329Z" }, + { url = "https://files.pythonhosted.org/packages/9d/21/0941a6b93a58b59a1ec75e5333bf06929b671309c43c0cd201c172d9c39f/yarl-1.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:bc3ac7bf569f6b64dad04dd7808c7872dae8a97df657856eac05e9b7e3614a85", size = 97697, upload-time = "2026-09-15T19:31:35.855Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ed/2f3129bbcc9a5c8ba12cc2b29d8060a3bab9c8043c456cfd4b5ca3188890/yarl-1.25.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:25868beca8b6765f8f7d0e11fe6dd7c66dd4b0793b9500286d20cc92352126a5", size = 143623, upload-time = "2026-09-15T19:31:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/f1bc3390fdca352826676b531d0712736f156919090206700421d46b2c37/yarl-1.25.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:10b2fd95332f0d716d5eee3c9fb2ce8eada19082de7fee83d32e37992fd75c26", size = 104011, upload-time = "2026-09-15T19:31:40.25Z" }, + { url = "https://files.pythonhosted.org/packages/a8/aa/50acc5c3e5da04172ae3c281c75405af4d2ca911e16120ab0563f4dffb66/yarl-1.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f12afda4eea8c8994a76d4df1875c765194f5fbe8a9d197929ea303caee29ec", size = 103677, upload-time = "2026-09-15T19:31:42.46Z" }, + { url = "https://files.pythonhosted.org/packages/30/d2/7d1e0ab9f8390e1fbcede5a6dbf70d23c96ad09b8c5567f3a514d1ddb0e2/yarl-1.25.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14b79a30a93a3ce2e8832603fd0ab780ada281b0ba5110b519a634f2d7d7d1fc", size = 115392, upload-time = "2026-09-15T19:31:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/71/e1/5ba1e3a2a22139213655e760919038e8ed7e2d4a99826d0bbddb3beb96e5/yarl-1.25.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bd6340d20ae2c7ca719b87b426e808e90743b676d05d4c26c4fb5ca71f41184", size = 107493, upload-time = "2026-09-15T19:31:46.273Z" }, + { url = "https://files.pythonhosted.org/packages/f5/53/780653d5e0f73831f467cf13548912e5eec97f21dc49fc8daf21da027df4/yarl-1.25.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:126a2533570c554719ca40a1288fdee1700b6bc82e7131aa69fa85252d92e651", size = 122537, upload-time = "2026-09-15T19:31:48.654Z" }, + { url = "https://files.pythonhosted.org/packages/03/92/d54fa70236c6036271c9c9c09fd978df5cbe3ef49ef6c46e9b833476d215/yarl-1.25.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a3faadac7d812ddac258feb57b9846b60c1b437c4f4b9ad42595c6f6fe4390df", size = 126170, upload-time = "2026-09-15T19:31:50.872Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b7/a82a49bf88340b837ef6972b508a1604ae377b9e6904b46b10cf5f1cf925/yarl-1.25.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be80550d9bfe83d9b62398a37081a90434e6df2d978ec345c3d2820de6beddab", size = 117012, upload-time = "2026-09-15T19:31:53.189Z" }, + { url = "https://files.pythonhosted.org/packages/ef/78/5d684b411e3f3602464ee9b538db48205038f8605872985f61efb809ced0/yarl-1.25.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e07595c7d6f4db270ceede356a1bd1c07a34f1c26f958d1ed0cd7b48e0d2bba3", size = 114950, upload-time = "2026-09-15T19:31:55.694Z" }, + { url = "https://files.pythonhosted.org/packages/2f/11/51d82b852c64f7fad0fc7a7ff3031517204887e874c722bbca839c0b23ac/yarl-1.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eb96ed1ae6c7d072d60840c0434aef07a2df611812810807fbc54263a6053e9a", size = 115428, upload-time = "2026-09-15T19:31:57.966Z" }, + { url = "https://files.pythonhosted.org/packages/e4/49/9d1978049bf646b9ea918313926453c6901b71c92f097467777d47d36a88/yarl-1.25.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3feb99222553a8cbedfa52c2f59dd84c3f50d5b582c728d522caf8d72769a54b", size = 108428, upload-time = "2026-09-15T19:32:00.048Z" }, + { url = "https://files.pythonhosted.org/packages/43/35/7b8f1ebb45d7ec3dda7d1909bf44f458de41ef91e2937f107733582a5166/yarl-1.25.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a2ed0ba415ccdf08f14bf544cb78346d0f76086707ffee24921a2c84dbf1305a", size = 121961, upload-time = "2026-09-15T19:32:02.436Z" }, + { url = "https://files.pythonhosted.org/packages/63/d6/d8b689ab7ca26edeb85f6ff28812aac7a25376eefc1780e303a7bfbaceff/yarl-1.25.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2b49375d22299b0a834c2bca72f39aaecc270d96fb24c30424899676f487b22a", size = 114961, upload-time = "2026-09-15T19:32:04.456Z" }, + { url = "https://files.pythonhosted.org/packages/cf/d5/1a1798ea4dc6b7ee3260010a27907ebc697c95dae99817d817ed446d24aa/yarl-1.25.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ef74070ac553c59eb4f04258722066d6c6135b7baa03b2e9f2da65c096e96d98", size = 120036, upload-time = "2026-09-15T19:32:06.5Z" }, + { url = "https://files.pythonhosted.org/packages/91/8d/b1b35ed7903da6669b1d367cb2c09436acd4ff508029b4f39a0c0c2058fc/yarl-1.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0a66db89ea473abeac4b70523cafd94db3772380e565f9d28af7a179b7af71fa", size = 117276, upload-time = "2026-09-15T19:32:09.401Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8f/4db01cef62caff0d7a4593ed694fb8a41a27a11158cab80d290221f13e57/yarl-1.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:1f51020b2eb8a003c84925638ec63c21a750a4bddd3a22ec8eac6a742dadf1b9", size = 101945, upload-time = "2026-09-15T19:32:11.545Z" }, + { url = "https://files.pythonhosted.org/packages/c0/5e/3ce00497c5c0babb74d4130c10c3828ccd215b4819d12020c42429f991ac/yarl-1.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:b10dd0557ba422715b5206b3743192135a6022acca8baec51aa127d0a75db8fe", size = 97270, upload-time = "2026-09-15T19:32:14.127Z" }, + { url = "https://files.pythonhosted.org/packages/80/cf/54023edfab7aa773b860503db0c56e962ccab0922803ee97988c176ea090/yarl-1.25.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:a9ca696eb02e5c02a8afd872ada510eba9b7fe6e68b9572c2e9a9b1941e31e2e", size = 143975, upload-time = "2026-09-15T19:32:16.416Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a8/e6c1be0e6761d0f2d10bbf33a3e1e02b99dc83874d92945d7b461a72481e/yarl-1.25.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a5877f2255aab518ebe528289037699201d5dc5f045f2396cb30aa02db22f57f", size = 104018, upload-time = "2026-09-15T19:32:18.364Z" }, + { url = "https://files.pythonhosted.org/packages/6e/bb/dda344765ffd3430afe1a1c66c866a57fae67786537d4f14607df6505ac1/yarl-1.25.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7a5c3115595995779ee21f2567035793911c3802a43c74f3fbb0314929ec67ac", size = 104156, upload-time = "2026-09-15T19:32:20.459Z" }, + { url = "https://files.pythonhosted.org/packages/e5/5f/ed1538bcd06009fe990d6d283dd7667f639e62a81e35c6d8c6ef6c08fb3c/yarl-1.25.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77e5099b99b37f3cf79c246998ca9f7313a78054cd1809ec46bc1afad47e1c4c", size = 116025, upload-time = "2026-09-15T19:32:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/a2/af/2185daf56b99830d3356ecfada46faaa49945de6626e842b7728088d4980/yarl-1.25.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6efaf45df6a849cef613a03a94c845647456662f85438c886bb67a9c027c8c2c", size = 106985, upload-time = "2026-09-15T19:32:24.749Z" }, + { url = "https://files.pythonhosted.org/packages/c1/65/bc1ae564fb4b04a30b6a8f250e787772581c57e4c3d5cf07ac3359de3103/yarl-1.25.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d5f90e44653c4e0f78501ed9bb7d3fce835a8d62b7c6ed0cb16557534087e743", size = 123030, upload-time = "2026-09-15T19:32:27.084Z" }, + { url = "https://files.pythonhosted.org/packages/6a/3e/e2afcde10d74e53b3fa889960991efb3019beda2b1682a01de720a302056/yarl-1.25.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:632da579b2d879f6bad20f2cfa35ded1efe2f4f77f8abb26a6234a5b236acd2f", size = 126765, upload-time = "2026-09-15T19:32:29.332Z" }, + { url = "https://files.pythonhosted.org/packages/a2/be/415b00c0fe5a0615b062a456b26623d7ec91c2bee20faea1a14045aa0469/yarl-1.25.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:30eec96e8a91bd588ce897c9543f6d5d8d34b28fbcba28a4dedf20ebeae9fe57", size = 117199, upload-time = "2026-09-15T19:32:31.49Z" }, + { url = "https://files.pythonhosted.org/packages/97/27/3d8c63ddd3e8bcfd033748ab93876678ce59bacd66e4cb1ed851c9c5b37e/yarl-1.25.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:12b6bc4906e11f5e1a1cdcb12296e7afbd366c783cc8073403cd2fb74334e453", size = 115187, upload-time = "2026-09-15T19:32:34.137Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/7a81d0be1a502a26a0d4326c6f2ecb736c824f570ea1c6529f2b0b227b50/yarl-1.25.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9d6ed3d17bccce4c05343e1ca8da13bc5c02c812a4e7282ddd05e8769322d3fc", size = 116085, upload-time = "2026-09-15T19:32:36.438Z" }, + { url = "https://files.pythonhosted.org/packages/f0/69/39fff459916aa0fab42215dc47b759586fd80f94aa56dfc4a7c15ba6e0dc/yarl-1.25.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:f38a70074041d3b7e138e452799f5174198bae5bd5ab2000917badf403908c5f", size = 107996, upload-time = "2026-09-15T19:32:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/c0/39/80b9a55a3335590451d9ecf3eb593a8c635351f4c905ef056d7e8a8fd9e7/yarl-1.25.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca89e4e21854ed27ec753297dde84b16c9f8e53b14a4866fb44457d643c19f8", size = 122549, upload-time = "2026-09-15T19:32:41.151Z" }, + { url = "https://files.pythonhosted.org/packages/42/7d/a179c6757818bb59372a4adafd09f7f26a3b4a0f04c3ae404b544c0b0c82/yarl-1.25.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1ab7618921a93767387a4b83776f751588f5b5ae9bb5bc96620e2e2e00bca868", size = 115107, upload-time = "2026-09-15T19:32:43.072Z" }, + { url = "https://files.pythonhosted.org/packages/32/2b/a773ac867e4ab53a98ed98e5cefe3bae31e6f550252ca9d1de266f1a40c5/yarl-1.25.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0ae12ff2b805fa02c4dab838005caef735e39986322698c48588d3beacb65c62", size = 120666, upload-time = "2026-09-15T19:32:45.061Z" }, + { url = "https://files.pythonhosted.org/packages/bc/41/52be6505e85b0f76b4f85b01b5de7e06a0512201abc2c95e14e099549174/yarl-1.25.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90c30ed53546da833c700115c0064c22120d1b1560f474699fd31f22dd668233", size = 117505, upload-time = "2026-09-15T19:32:47.177Z" }, + { url = "https://files.pythonhosted.org/packages/f5/01/349c0386caedbbe488d519f252df54efac8a1459282d466c474bdd84a620/yarl-1.25.1-cp314-cp314-win_amd64.whl", hash = "sha256:acfa7e22aa6c6e7a5996a41d275bfa01efa7ea56ab890590280e9063e2cf5c1b", size = 103446, upload-time = "2026-09-15T19:32:49.615Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f0/8ec63180f77912f0dc4e5a42760cb8c08d20da1d5ace3578a01b84d1f3d8/yarl-1.25.1-cp314-cp314-win_arm64.whl", hash = "sha256:8e7d98cdbb6d71e726f7d525952867096053d1f290dd4e3c50d7d313a136f414", size = 99159, upload-time = "2026-09-15T19:32:51.686Z" }, + { url = "https://files.pythonhosted.org/packages/47/7d/92d2220d6886b70ab1ed8579533ac2af2dfac716d5d929001daff7986df9/yarl-1.25.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d21f0fa80a02d05299207eeaafef345d812ace96d5306e4ef265e1d419a615fa", size = 150071, upload-time = "2026-09-15T19:32:53.911Z" }, + { url = "https://files.pythonhosted.org/packages/64/fc/b245e448124bcda9340df38e3553fa222b50260fca027a84095e9bd8642d/yarl-1.25.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17c9877a89fb6e2bca6f9087eb24cd7fb434653946ef5075e470d23d49b52287", size = 106780, upload-time = "2026-09-15T19:32:56.443Z" }, + { url = "https://files.pythonhosted.org/packages/51/e2/9a6ce2e334ebf218a30335ae76fb1696459430d42f733b8cb0d7d65b84d3/yarl-1.25.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:29273edf1530e397bd07cb784db1fbe0d2590b77569f2e24679a9c0a2d763b94", size = 107361, upload-time = "2026-09-15T19:32:58.827Z" }, + { url = "https://files.pythonhosted.org/packages/ed/70/66e8c76b569b450d16e190f15071c916c3df70b0e33927e415ac497cf0c2/yarl-1.25.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7abffdf37af1cec6a2ad69b827aa84320db5894791bc8ed932dc93fb274b7e9", size = 114396, upload-time = "2026-09-15T19:33:02.24Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/0d82838a05c57fdc05bc8b66e8c92dcc0df15e27463a5f163142d521c682/yarl-1.25.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2239a02249d9326655419e0168a28ca9008938eaab31dc29fc875c217927a6c0", size = 104882, upload-time = "2026-09-15T19:33:04.494Z" }, + { url = "https://files.pythonhosted.org/packages/86/d4/ea08615c4edaa6049a13a2f1128944d068d1893abda7d708d4d7ea01599a/yarl-1.25.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:664ec6a520b74a1df2810666eb67695fcb77fa663e6ea0a25aaf2e529cb24dfa", size = 119485, upload-time = "2026-09-15T19:33:06.583Z" }, + { url = "https://files.pythonhosted.org/packages/1a/82/0898bdce9b1ae403b308b9c733d0d24af4a3464270c2c081f457b16c3e0d/yarl-1.25.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4f1c91f5a5980a937ff8e238e98e6897e1ad74a4b1e2c0d68c73b5ffbb3f5c0b", size = 122490, upload-time = "2026-09-15T19:33:08.653Z" }, + { url = "https://files.pythonhosted.org/packages/d1/38/97d79b81c342b78246cfedb74809e68841f3198d21653e10d3232bd9c622/yarl-1.25.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c88edaec8c349ad4c5ad4c486a3defcc4b80ceb2f074436ffa0a87caf5e76a6", size = 115336, upload-time = "2026-09-15T19:33:11.056Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9d/2577896554cd310dc470adb6da0b7dd0b435cb63e2565204a7ac240e504c/yarl-1.25.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:35dcbea443fafb3eece757ad4e514560ddeb6c34cfae1582c620d7b293d7feee", size = 111825, upload-time = "2026-09-15T19:33:13.204Z" }, + { url = "https://files.pythonhosted.org/packages/29/6b/7ac49d8ba84a5c4bd73415a4c949d22c749cb3762579b3d50e48019a78aa/yarl-1.25.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:882569ff613758cac762a457a5d72d6e211b28d4bcfea89d1d71ea942b02eac0", size = 114655, upload-time = "2026-09-15T19:33:15.553Z" }, + { url = "https://files.pythonhosted.org/packages/e5/18/e5942a16723f5b72f9b1297fd5a85a54f6300cd15c0dcb5005b90cd89156/yarl-1.25.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d0f1489233a254bb3643d2f05de7d59019254d81daeca6b9162fe9edef57e0c7", size = 106395, upload-time = "2026-09-15T19:33:17.599Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2d/549fa46240781513ebc47ae7eb418df428a163a2a3d644cc9cbb3ecb7846/yarl-1.25.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f41753a76f4f63927d03a0d8ba8f5ce0f2083bec29a8cfaccc55371b1564b96b", size = 119277, upload-time = "2026-09-15T19:33:19.973Z" }, + { url = "https://files.pythonhosted.org/packages/76/16/4763f78dcdc0b3b9fb3842b04afe72b9320857c6a69300c62a0eab03d119/yarl-1.25.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fb0eb4955adf0579001581f2f71a126e8781ba61bcd120f127b0401163c6c2d", size = 112504, upload-time = "2026-09-15T19:33:22.464Z" }, + { url = "https://files.pythonhosted.org/packages/ae/b4/974e3edfe0d188393ce1cb9de400111c63fe61f4eb3b772a500d84c970d1/yarl-1.25.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a1e32763e641a1566507d90a8d3b19bfc3cc04a9d4e5ae3e32189874ed4b58a3", size = 116243, upload-time = "2026-09-15T19:33:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/b0/aa/157b940428da80c104ca09666a740e51c94963df65d5b112e06b52e4d7a8/yarl-1.25.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65b5b2066651b7432d389e9799d979c703bcc6ef44266bb8153ef54e91e4aab3", size = 115822, upload-time = "2026-09-15T19:33:26.886Z" }, + { url = "https://files.pythonhosted.org/packages/7e/af/19fbdce41412e1b96825544cc52cd7029d3724655d0988237972f078bd29/yarl-1.25.1-cp314-cp314t-win_amd64.whl", hash = "sha256:734f6e5400352ac4254456003d462866c684703570929cff7a7bde015d0cb371", size = 107386, upload-time = "2026-09-15T19:33:29.009Z" }, + { url = "https://files.pythonhosted.org/packages/2a/99/f6431c8968e89be608d74b28ae2d024521b2953f27dd44e0dece5e04f67a/yarl-1.25.1-cp314-cp314t-win_arm64.whl", hash = "sha256:287e99ff5aa4dc1c7630bfc683ded6f106d756c99dec432a2d7f197a784f51c6", size = 102094, upload-time = "2026-09-15T19:33:31.151Z" }, + { url = "https://files.pythonhosted.org/packages/c7/3b/4f51eab40c2eabea6c3d5b121dff4b8988dc35087732ffede12d2be8b8dd/yarl-1.25.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9b1bdaae98bc016825dd3c9d8ee1832f829b3341f9cc6ebd1a1b0a7fef7367cc", size = 143875, upload-time = "2026-09-15T19:33:33.52Z" }, + { url = "https://files.pythonhosted.org/packages/41/05/bbd58fc063f5f299a883f810760b265ca26c8167c91cb9a494d0fe2387e1/yarl-1.25.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:e7011b8fb8c4054bf0c12e5edc6cd83778b0028e99ce59b18586ed036f92cfdc", size = 104028, upload-time = "2026-09-15T19:33:36.22Z" }, + { url = "https://files.pythonhosted.org/packages/12/ee/2fba0aecb52e7020e189f684148783aa0b9cfa3b3bfb0b400646eef70ad4/yarl-1.25.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:f074e8d4aa0a5798920ddb6de3d08b228c614ff3724c3e8bd7577f4bafea867b", size = 104041, upload-time = "2026-09-15T19:33:38.86Z" }, + { url = "https://files.pythonhosted.org/packages/38/35/884beab53ed88c7247d1671972b5ef116f7351fe0c7e6de8c3558372cb16/yarl-1.25.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d42e7e3ca399555578b4d617e3a6ecf13371b3743a115995fa010c7bf341459", size = 116018, upload-time = "2026-09-15T19:33:44.265Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/1a91b685afb55cc18608443ace95280e96e263a97565811732b6788d3269/yarl-1.25.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aa4ed3dd308548f9e707d9caaf005d2d7f8c1e7868f858dfeb47fe76e16b391d", size = 107033, upload-time = "2026-09-15T19:33:46.45Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c1/58b379fcb1d68d907b7fcf75200c44321896509b2a6a74abbb4b19d864d2/yarl-1.25.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42a66563d8cc056ee32e6191e05097a7b2b3bc302e0bc3133daf8710eb18bd26", size = 123257, upload-time = "2026-09-15T19:33:48.631Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2d/1fe96cf5c2aeab10095e48f38585cf5a8451fb7253234822398e52aa5336/yarl-1.25.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:98d370568f393215d605304cdb77b3d5539bd192c75b623c7304c42c8d6d8273", size = 126745, upload-time = "2026-09-15T19:33:50.999Z" }, + { url = "https://files.pythonhosted.org/packages/ad/60/8674394ce43f4dadae573a1d6f451716438e9eab7d7fe8d643c673a32d85/yarl-1.25.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23bf5b403c879a54964e0feac7285688e04bb220074878d737d331522da0a5bf", size = 117255, upload-time = "2026-09-15T19:33:53.456Z" }, + { url = "https://files.pythonhosted.org/packages/2f/72/0faa30e02605d56127d42bb987dcc97da3863b7bf70b9bfbf5f739c05e30/yarl-1.25.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d45673badd08456d0340e9364eddafe1c53a9d2896424294de4d7dd71ad3ee57", size = 115166, upload-time = "2026-09-15T19:33:55.669Z" }, + { url = "https://files.pythonhosted.org/packages/8c/90/9a46eac564c437e128285c5c1d7bb385d268394e9209d84f6bf4a14471ef/yarl-1.25.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:0136d640dfa9b0523853e411430a99f8a91eca85774c6420285a33b755bc6de3", size = 116082, upload-time = "2026-09-15T19:33:57.78Z" }, + { url = "https://files.pythonhosted.org/packages/ad/38/4b1a686a3758878f93d2f1ea943f5a165f3555769cd16e761cfd0efdca17/yarl-1.25.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:59ba3a6e1aa8cfe5adf4bd270fd965db21955401b7ca6f1696010c55ed4daec2", size = 108048, upload-time = "2026-09-15T19:34:00.25Z" }, + { url = "https://files.pythonhosted.org/packages/da/14/f348eb967f31a58348612a2b93bd8a2ab664548e2b5e879cac7f592f7201/yarl-1.25.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:87796fedc3ba97ec14fab55acb48584276e6c1e4c1e89c422bda62c838e754a9", size = 122775, upload-time = "2026-09-15T19:34:02.455Z" }, + { url = "https://files.pythonhosted.org/packages/ab/e9/4f7b79700f88cb9e8bb66f8b54f9bce1844c013a2c39fdc47112e9334c95/yarl-1.25.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bd0912757081f89b107d6c00b2ff8a194401b0b87eadcf4481de2b865a8fd44f", size = 115096, upload-time = "2026-09-15T19:34:05.283Z" }, + { url = "https://files.pythonhosted.org/packages/cf/37/f9cb020331997d3eb887bd28d5410ecfd3d80bf163c23d7cec490d78dade/yarl-1.25.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:b51c159a9794633f5e0db7ecec7b2b6e3734eca1f5d17dc989ff3552a43ff78b", size = 120655, upload-time = "2026-09-15T19:34:07.382Z" }, + { url = "https://files.pythonhosted.org/packages/12/83/52fceb22891a41f168db7ec22fd1d81e06b6a0b8d9f70921bd3e785defd0/yarl-1.25.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:319e070a01db9920fb63761843f96a104c8e2b9427266731810dc1e22595b17c", size = 117488, upload-time = "2026-09-15T19:34:09.988Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5c/ce6c4ff1247fcbe4b33d462c23a097106d909b173fde7042bc52290466e2/yarl-1.25.1-cp315-cp315-win_amd64.whl", hash = "sha256:a2059a2d891bd156bc5184e7ab7a56e78a84dfcfdeac8c501b552533ad1c36ee", size = 103434, upload-time = "2026-09-15T19:34:12.56Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/766a906b0704fb26d52b19dc22bed48a8ba0544203b70dcf44da350e8194/yarl-1.25.1-cp315-cp315-win_arm64.whl", hash = "sha256:a78b50b4f7918a3de71105d5c0b93bbc57bb8339a4d03a9dfd449f9068e76f3d", size = 99155, upload-time = "2026-09-15T19:34:15.132Z" }, + { url = "https://files.pythonhosted.org/packages/bd/d3/a1d09b32cb6ab14f66b44939f5b4255b8b9e747aef3974af1d5d80ccc2fd/yarl-1.25.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:b5402a340723fa7da00b5cff987ddab61276be6d11251ea71ae02bcac54890d8", size = 149284, upload-time = "2026-09-15T19:34:17.452Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/fe554d879692650bca70bfbc0df124e82e4d2bb7456f698c7756f1279a96/yarl-1.25.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:eda19ea5ee88742f47a2340816e6f2d40b53bed3ab5b69794769f36af9f35bb4", size = 106399, upload-time = "2026-09-15T19:34:21.474Z" }, + { url = "https://files.pythonhosted.org/packages/e1/4b/e7af56177ac8d40094c82d7728224c0b8472157d50d362e5fb3b014b2bc8/yarl-1.25.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:75baa6cf9b6d1c52f3e111a130e202fd8cf0a5b3a066c3f73d615e885092e4ec", size = 106968, upload-time = "2026-09-15T19:34:23.619Z" }, + { url = "https://files.pythonhosted.org/packages/da/4f/2df41fd738d46f23ef829ae8b4468d94bb6070038fc6dfab6165ed44fea8/yarl-1.25.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbcef5a9119ef653653132cccaf999b30a0af6f33bb0a4ba80bec30056868487", size = 114717, upload-time = "2026-09-15T19:34:25.879Z" }, + { url = "https://files.pythonhosted.org/packages/69/ea/002b66df53bbd1aed1c23358ff99c9bdc744fe3f4d2740e1b2fdd7192885/yarl-1.25.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7efc9f082dfed77c316edffa9deb52888e1bc6789171887cc1f68e06d65465c8", size = 105198, upload-time = "2026-09-15T19:34:28.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/7c/95c8bc0c8f97d71e59c94525ad60d76f5c57d3f2820f08137ca8b9f0542a/yarl-1.25.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fe01645169a2112aa1d4ebc3e4c5f029c5c8f97adfc32e5d37c993b39a994d75", size = 120271, upload-time = "2026-09-15T19:34:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7a/6fe9da56ec77927baa669fd86c39c567ce6205bab53d581082c6744c8ae7/yarl-1.25.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1c557dfd5e3db046053a0bdc72261ade790ebe8e2c7a41b36b0ca1f14cb95f3", size = 123572, upload-time = "2026-09-15T19:34:32.73Z" }, + { url = "https://files.pythonhosted.org/packages/8b/83/35f222d17fa70a14c7c74fdf112ccf5515e0c2a87082b1f9b99f7693bf57/yarl-1.25.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ce4d6ccafb33d39bd78444612d14938ead674c25702ded2ee9c54a47735d225", size = 115228, upload-time = "2026-09-15T19:34:35.344Z" }, + { url = "https://files.pythonhosted.org/packages/da/6f/fbaaf619423578a7d898d0f226ae47bc1906c293865c416d83c17b828b0e/yarl-1.25.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80a063f8297fc796296f00f100be520f209b23dc98f93ce8eba6ee7122598209", size = 111618, upload-time = "2026-09-15T19:34:37.656Z" }, + { url = "https://files.pythonhosted.org/packages/ba/78/7383278f1b3cf8e0496bd95b3281a7b09b89217b6b428db24c6b99b3deca/yarl-1.25.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:7cb414a73e21a7ab58254926073f2930cb22f5b4314ea4260a687e2b3fd4dce3", size = 114839, upload-time = "2026-09-15T19:34:40.099Z" }, + { url = "https://files.pythonhosted.org/packages/62/49/5506e5b6d29aab91bd845cc9016d88c3d3f81b81bc242b8100bdd5737825/yarl-1.25.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:85a18376073f8a39aa07be34f9fc77e2869aa72c55c441efdd2cf79a0407504d", size = 106212, upload-time = "2026-09-15T19:34:42.768Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/19877c193b7c5929f4b07118c18bbe390f3fadd0f59dd98c0cd12b31fa5c/yarl-1.25.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:77716e245c90f058466a05e6a465bb8600f767a8f4b18b4d40f3aff958e5f73c", size = 119985, upload-time = "2026-09-15T19:34:44.976Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6c/0a46fbbf9ecbcbd0cc20d2193394254b9e19817f22c814aab60f99847400/yarl-1.25.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:1e80dcf1446e1b080b1932b0d103c464a04112f5bc31f0f983ad418172063cde", size = 112081, upload-time = "2026-09-15T19:34:47.45Z" }, + { url = "https://files.pythonhosted.org/packages/ef/30/93f5d471230c74ccd06255d0842739f86551f937f9e63a5e947853c6244a/yarl-1.25.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:bdc8d8b8c22e9e43ac68316b5e6cf083dec537f4ec213cb4aa967b583bc3fa64", size = 116995, upload-time = "2026-09-15T19:34:49.972Z" }, + { url = "https://files.pythonhosted.org/packages/2b/80/c386593035ee3f9c6c6af0847b5578f2830c674794a9d7701b744a3ebd42/yarl-1.25.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbf531053a0935f2e871bcd4753f90313688772ff8c017f5ea402e315a78c1f", size = 115570, upload-time = "2026-09-15T19:34:52.628Z" }, + { url = "https://files.pythonhosted.org/packages/38/02/eef443559563ef8f2e10469387b8b1e97cb5efee95b288a56da60801f7ee/yarl-1.25.1-cp315-cp315t-win_amd64.whl", hash = "sha256:b13b88747769537f3d32e89e3a735da10c0a9e35d7322928c701b5f93d3afffd", size = 106811, upload-time = "2026-09-15T19:34:54.935Z" }, + { url = "https://files.pythonhosted.org/packages/88/91/41e284ca2cf5211e05dae031d126a3668aea88fa759df56e7e35c6ad25ba/yarl-1.25.1-cp315-cp315t-win_arm64.whl", hash = "sha256:783dd1467083f4d3f7722ad6a313f24c173e7571372738fcb7a6e6d1ba48df25", size = 101804, upload-time = "2026-09-15T19:34:57.231Z" }, + { url = "https://files.pythonhosted.org/packages/54/22/318c7980066769c6bcd9221ed2248294f5698811da099013098c670565ed/yarl-1.25.1-py3-none-any.whl", hash = "sha256:681c758b0490f9e96b78e5fa8e8dc6e648e9185bb6eaebe73183c33ea0c445f3", size = 63617, upload-time = "2026-09-15T19:34:59.616Z" }, ] [[package]]