From 7f12ee0cdde6d7e1f0b3d0e109e959d32769ff3b Mon Sep 17 00:00:00 2001 From: Maksim Obukhov Date: Thu, 15 Oct 2026 13:03:33 +0200 Subject: [PATCH 01/26] feat(js-sdk): start streaming commands without retaining output Add an internal Commands.startStreaming() that starts a background command whose stdout/stderr only reach the callbacks and are not accumulated on the CommandHandle, so long-lived processes do not grow memory without bound. --- .../src/sandbox/commands/commandHandle.ts | 11 ++++--- packages/js-sdk/src/sandbox/commands/index.ts | 32 ++++++++++++++++--- .../sandbox/commands/startStreaming.test.ts | 29 +++++++++++++++++ 3 files changed, 63 insertions(+), 9 deletions(-) create mode 100644 packages/js-sdk/tests/sandbox/commands/startStreaming.test.ts diff --git a/packages/js-sdk/src/sandbox/commands/commandHandle.ts b/packages/js-sdk/src/sandbox/commands/commandHandle.ts index 3110d14f3b..530ded7f43 100644 --- a/packages/js-sdk/src/sandbox/commands/commandHandle.ts +++ b/packages/js-sdk/src/sandbox/commands/commandHandle.ts @@ -121,7 +121,8 @@ export class CommandHandle private readonly handleCloseStdin?: ( opts?: CommandRequestOpts ) => Promise, - private readonly checkHealth?: SandboxHealthCheck + private readonly checkHealth?: SandboxHealthCheck, + private readonly retainOutput = true ) { this._wait = this.handleEvents() } @@ -257,12 +258,12 @@ export class CommandHandle > { const stdoutRest = this.stdoutDecoder.decode() if (stdoutRest) { - this._stdout += stdoutRest + if (this.retainOutput) this._stdout += stdoutRest yield [stdoutRest as Stdout, null, null] } const stderrRest = this.stderrDecoder.decode() if (stderrRest) { - this._stderr += stderrRest + if (this.retainOutput) this._stderr += stderrRest yield [null, stderrRest as Stderr, null] } } @@ -283,7 +284,7 @@ export class CommandHandle stream: true, }) if (out) { - this._stdout += out + if (this.retainOutput) this._stdout += out yield [out as Stdout, null, null] } break @@ -292,7 +293,7 @@ export class CommandHandle stream: true, }) if (out) { - this._stderr += out + if (this.retainOutput) this._stderr += out yield [null, out as Stderr, null] } break diff --git a/packages/js-sdk/src/sandbox/commands/index.ts b/packages/js-sdk/src/sandbox/commands/index.ts index a9905d084d..1d043b2b8e 100644 --- a/packages/js-sdk/src/sandbox/commands/index.ts +++ b/packages/js-sdk/src/sandbox/commands/index.ts @@ -407,9 +407,25 @@ export class Commands { return opts?.background ? proc : proc.wait() } + /** + * Start a background command whose output is only delivered to the + * `onStdout`/`onStderr` callbacks and never accumulated on the handle. + * Meant for long-lived streaming processes. + * + * @hidden + * @internal + */ + async startStreaming( + cmd: string, + opts?: Omit + ): Promise { + return this.start(cmd, opts, false) + } + private async start( cmd: string, - opts?: CommandStartOpts + opts?: CommandStartOpts, + retainOutput = true ): Promise { if ( opts?.stdin === false && @@ -448,14 +464,21 @@ export class Commands { } ) - return this.createHandle(events, clearStartTimeout, cleanup, opts) + return this.createHandle( + events, + clearStartTimeout, + cleanup, + opts, + retainOutput + ) } private async createHandle( events: AsyncIterable, clearStartTimeout: () => void, cleanup: () => void, - opts?: Pick + opts?: Pick, + retainOutput = true ): Promise { try { const pid = await handleProcessStartEvent(events) @@ -471,7 +494,8 @@ export class Commands { undefined, (data, stdinOpts) => this.sendStdin(pid, data, stdinOpts), (stdinOpts) => this.closeStdin(pid, stdinOpts), - this.checkHealth + this.checkHealth, + retainOutput ) } catch (err) { cleanup() diff --git a/packages/js-sdk/tests/sandbox/commands/startStreaming.test.ts b/packages/js-sdk/tests/sandbox/commands/startStreaming.test.ts new file mode 100644 index 0000000000..1f5e326e75 --- /dev/null +++ b/packages/js-sdk/tests/sandbox/commands/startStreaming.test.ts @@ -0,0 +1,29 @@ +import { assert } from 'vitest' +import { sandboxTest } from '../../setup.js' + +sandboxTest( + 'streaming command delivers output only to callbacks', + async ({ sandbox }) => { + let stdout = '' + let stderr = '' + const cmd = await sandbox.commands.startStreaming( + 'echo out; echo err >&2', + { + onStdout: (data) => { + stdout += data + }, + onStderr: (data) => { + stderr += data + }, + } + ) + + const result = await cmd.wait() + + assert.equal(stdout, 'out\n') + assert.equal(stderr, 'err\n') + assert.equal(cmd.stdout, '') + assert.equal(cmd.stderr, '') + assert.equal(result.exitCode, 0) + } +) From 5aaa0378c79e47ad21b7f13f711adb90654c98cb Mon Sep 17 00:00:00 2001 From: Maksim Obukhov Date: Thu, 15 Oct 2026 13:11:48 +0200 Subject: [PATCH 02/26] feat(python-sdk): start streaming commands without retaining output Add an internal retain_output flag to Commands._start and both command handles, so long-lived processes can stream stdout/stderr through callbacks or iteration without accumulating it in memory. Mirrors the JS Commands.startStreaming(). --- .../e2b/sandbox_async/commands/command.py | 10 ++++++- .../sandbox_async/commands/command_handle.py | 14 +++++++--- .../e2b/sandbox_sync/commands/command.py | 5 +++- .../sandbox_sync/commands/command_handle.py | 14 +++++++--- .../commands/test_start_streaming.py | 28 +++++++++++++++++++ .../commands/test_start_streaming.py | 27 ++++++++++++++++++ 6 files changed, 88 insertions(+), 10 deletions(-) create mode 100644 packages/python-sdk/tests/async/sandbox_async/commands/test_start_streaming.py create mode 100644 packages/python-sdk/tests/sync/sandbox_sync/commands/test_start_streaming.py diff --git a/packages/python-sdk/e2b/sandbox_async/commands/command.py b/packages/python-sdk/e2b/sandbox_async/commands/command.py index 514add11af..538554da6b 100644 --- a/packages/python-sdk/e2b/sandbox_async/commands/command.py +++ b/packages/python-sdk/e2b/sandbox_async/commands/command.py @@ -300,6 +300,7 @@ async def _start( request_timeout: Optional[float], on_stdout: Optional[OutputHandler[Stdout]], on_stderr: Optional[OutputHandler[Stderr]], + retain_output: bool = True, ) -> AsyncCommandHandle: events = as_stream( self._rpc.start( @@ -324,7 +325,12 @@ async def _start( ) return await self._create_handle( - events, "start process", request_timeout, on_stdout, on_stderr + events, + "start process", + request_timeout, + on_stdout, + on_stderr, + retain_output, ) async def connect( @@ -372,6 +378,7 @@ async def _create_handle( request_timeout: Optional[float], on_stdout: Optional[OutputHandler[Stdout]], on_stderr: Optional[OutputHandler[Stderr]], + retain_output: bool = True, ) -> AsyncCommandHandle: try: start_event = await first_event( @@ -392,6 +399,7 @@ async def _create_handle( pid, request_timeout ), check_health=self._check_health, + retain_output=retain_output, ) except Exception as e: try: diff --git a/packages/python-sdk/e2b/sandbox_async/commands/command_handle.py b/packages/python-sdk/e2b/sandbox_async/commands/command_handle.py index 2a68bdc840..714b13c38c 100644 --- a/packages/python-sdk/e2b/sandbox_async/commands/command_handle.py +++ b/packages/python-sdk/e2b/sandbox_async/commands/command_handle.py @@ -95,6 +95,7 @@ def __init__( Callable[[Optional[float]], Coroutine[Any, Any, None]] ] = None, check_health: Optional[Callable[[], Awaitable[Optional[bool]]]] = None, + retain_output: bool = True, ): self._pid = pid self._handle_kill = handle_kill @@ -103,6 +104,7 @@ def __init__( self._check_health = check_health self._events = events + self._retain_output = retain_output self._stdout_chunks: List[str] = [] self._stderr_chunks: List[str] = [] @@ -130,11 +132,13 @@ def _flush_decoders( events: List[Union[Tuple[Stdout, None, None], Tuple[None, Stderr, None]]] = [] out = self._stdout_decoder.decode(b"", final=True) if out: - self._stdout_chunks.append(out) + if self._retain_output: + self._stdout_chunks.append(out) events.append((out, None, None)) err = self._stderr_decoder.decode(b"", final=True) if err: - self._stderr_chunks.append(err) + if self._retain_output: + self._stderr_chunks.append(err) events.append((None, err, None)) return events @@ -159,12 +163,14 @@ async def _iterate_events( case Oneof(field="stdout", value=chunk): out = self._stdout_decoder.decode(chunk) if out: - self._stdout_chunks.append(out) + if self._retain_output: + self._stdout_chunks.append(out) yield out, None, None case Oneof(field="stderr", value=chunk): out = self._stderr_decoder.decode(chunk) if out: - self._stderr_chunks.append(out) + if self._retain_output: + self._stderr_chunks.append(out) yield None, out, None case Oneof(field="pty", value=chunk): yield None, None, chunk diff --git a/packages/python-sdk/e2b/sandbox_sync/commands/command.py b/packages/python-sdk/e2b/sandbox_sync/commands/command.py index a46a84dc98..6994e50533 100644 --- a/packages/python-sdk/e2b/sandbox_sync/commands/command.py +++ b/packages/python-sdk/e2b/sandbox_sync/commands/command.py @@ -310,6 +310,7 @@ def _start( stdin: bool, timeout: Optional[float], request_timeout: Optional[float], + retain_output: bool = True, ): events = as_stream( self._rpc.start( @@ -333,7 +334,7 @@ def _start( ) ) - return self._create_handle(events, "start process") + return self._create_handle(events, "start process", retain_output) def connect( self, @@ -371,6 +372,7 @@ def _create_handle( Union[process_pb.StartResponse, process_pb.ConnectResponse], Any, None ], action: str, + retain_output: bool = True, ) -> CommandHandle: try: start_event = events.__next__() @@ -387,6 +389,7 @@ def _create_handle( pid, request_timeout ), check_health=self._check_health, + retain_output=retain_output, ) except Exception as e: try: diff --git a/packages/python-sdk/e2b/sandbox_sync/commands/command_handle.py b/packages/python-sdk/e2b/sandbox_sync/commands/command_handle.py index d1edb9e622..e51ed401b5 100644 --- a/packages/python-sdk/e2b/sandbox_sync/commands/command_handle.py +++ b/packages/python-sdk/e2b/sandbox_sync/commands/command_handle.py @@ -42,6 +42,7 @@ def __init__( ] = None, handle_close_stdin: Optional[Callable[[Optional[float]], None]] = None, check_health: Optional[Callable[[], Optional[bool]]] = None, + retain_output: bool = True, ): self._pid = pid self._handle_kill = handle_kill @@ -50,6 +51,7 @@ def __init__( self._check_health = check_health self._events = events + self._retain_output = retain_output self._stdout_chunks: List[str] = [] self._stderr_chunks: List[str] = [] @@ -79,11 +81,13 @@ def _flush_decoders( events: List[Union[Tuple[Stdout, None, None], Tuple[None, Stderr, None]]] = [] out = self._stdout_decoder.decode(b"", final=True) if out: - self._stdout_chunks.append(out) + if self._retain_output: + self._stdout_chunks.append(out) events.append((out, None, None)) err = self._stderr_decoder.decode(b"", final=True) if err: - self._stderr_chunks.append(err) + if self._retain_output: + self._stderr_chunks.append(err) events.append((None, err, None)) return events @@ -109,12 +113,14 @@ def _handle_events( case Oneof(field="stdout", value=chunk): out = self._stdout_decoder.decode(chunk) if out: - self._stdout_chunks.append(out) + if self._retain_output: + self._stdout_chunks.append(out) yield out, None, None case Oneof(field="stderr", value=chunk): out = self._stderr_decoder.decode(chunk) if out: - self._stderr_chunks.append(out) + if self._retain_output: + self._stderr_chunks.append(out) yield None, out, None case Oneof(field="pty", value=chunk): yield None, None, chunk diff --git a/packages/python-sdk/tests/async/sandbox_async/commands/test_start_streaming.py b/packages/python-sdk/tests/async/sandbox_async/commands/test_start_streaming.py new file mode 100644 index 0000000000..5433a0d437 --- /dev/null +++ b/packages/python-sdk/tests/async/sandbox_async/commands/test_start_streaming.py @@ -0,0 +1,28 @@ +from e2b import AsyncSandbox + + +async def test_streaming_command_delivers_output_only_to_callbacks( + async_sandbox: AsyncSandbox, +): + stdout, stderr = [], [] + cmd = await async_sandbox.commands._start( + "echo out; echo err >&2", + envs=None, + user=None, + cwd=None, + stdin=False, + timeout=60, + request_timeout=None, + on_stdout=stdout.append, + on_stderr=stderr.append, + retain_output=False, + ) + + result = await cmd.wait() + + assert "".join(stdout) == "out\n" + assert "".join(stderr) == "err\n" + assert cmd.stdout == "" + assert result.stdout == "" + assert result.stderr == "" + assert result.exit_code == 0 diff --git a/packages/python-sdk/tests/sync/sandbox_sync/commands/test_start_streaming.py b/packages/python-sdk/tests/sync/sandbox_sync/commands/test_start_streaming.py new file mode 100644 index 0000000000..85b4453bff --- /dev/null +++ b/packages/python-sdk/tests/sync/sandbox_sync/commands/test_start_streaming.py @@ -0,0 +1,27 @@ +from e2b import Sandbox + + +def test_streaming_command_yields_output_without_retaining_it(sandbox: Sandbox): + cmd = sandbox.commands._start( + "echo out; echo err >&2", + envs=None, + user=None, + cwd=None, + stdin=False, + timeout=60, + request_timeout=None, + retain_output=False, + ) + + stdout, stderr = "", "" + for out, err, _ in cmd: + stdout += out or "" + stderr += err or "" + + result = cmd.wait() + + assert stdout == "out\n" + assert stderr == "err\n" + assert result.stdout == "" + assert result.stderr == "" + assert result.exit_code == 0 From 4919434bc1b301ea5109e4837dfa32ee86f81dd6 Mon Sep 17 00:00:00 2001 From: Maksim Obukhov Date: Thu, 15 Oct 2026 15:11:29 +0200 Subject: [PATCH 03/26] feat(acp-js): add @e2b/acp package with ACP framing helpers New package for running Agent Client Protocol agents in E2B sandboxes. This first part has the newline-delimited JSON framing with a validated per-message size limit, and the rules that keep the agent's tools in the sandbox: initialize (also inside a batch) turns off the client fs and terminal capabilities, and agent fs/* and terminal/* requests are answered with method-not-found. --- packages/acp-js/LICENSE | 9 ++ packages/acp-js/package.json | 69 ++++++++++++++ packages/acp-js/src/utils.ts | 134 ++++++++++++++++++++++++++++ packages/acp-js/tests/utils.test.ts | 134 ++++++++++++++++++++++++++++ packages/acp-js/tsconfig.json | 8 ++ packages/acp-js/tsdown.config.ts | 5 ++ packages/acp-js/vitest.config.mts | 16 ++++ pnpm-lock.yaml | 37 ++++++++ 8 files changed, 412 insertions(+) create mode 100644 packages/acp-js/LICENSE create mode 100644 packages/acp-js/package.json create mode 100644 packages/acp-js/src/utils.ts create mode 100644 packages/acp-js/tests/utils.test.ts create mode 100644 packages/acp-js/tsconfig.json create mode 100644 packages/acp-js/tsdown.config.ts create mode 100644 packages/acp-js/vitest.config.mts diff --git a/packages/acp-js/LICENSE b/packages/acp-js/LICENSE new file mode 100644 index 0000000000..4aa6529d10 --- /dev/null +++ b/packages/acp-js/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2025 FOUNDRYLABS, INC. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/acp-js/package.json b/packages/acp-js/package.json new file mode 100644 index 0000000000..f24018fa13 --- /dev/null +++ b/packages/acp-js/package.json @@ -0,0 +1,69 @@ +{ + "name": "@e2b/acp", + "version": "0.0.0", + "packageManager": "pnpm@10.34.5", + "description": "Run Agent Client Protocol (ACP) agents in E2B sandboxes", + "homepage": "https://e2b.dev/docs/agents/acp?utm_source=npm&utm_medium=referral&utm_campaign=package_homepage&utm_content=e2b-acp", + "license": "MIT", + "author": { + "name": "FoundryLabs, Inc.", + "email": "hello@e2b.dev", + "url": "https://e2b.dev" + }, + "bugs": "https://github.com/e2b-dev/e2b/issues", + "repository": { + "type": "git", + "url": "https://github.com/e2b-dev/e2b", + "directory": "packages/acp-js" + }, + "publishConfig": { + "access": "public" + }, + "sideEffects": false, + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "scripts": { + "prepublishOnly": "pnpm build", + "build": "tsc --noEmit && tsdown", + "dev": "tsdown --watch", + "test": "vitest run", + "typecheck": "tsc --noEmit", + "lint": "oxlint --config ../../.oxlintrc.json src tests", + "format": "prettier --write src/ tests/" + }, + "devDependencies": { + "@agentclientprotocol/sdk": "1.8.0", + "@types/node": "catalog:", + "@typescript/native": "catalog:", + "dotenv": "^16.4.5", + "tsdown": "catalog:", + "typescript": "catalog:", + "vitest": "catalog:" + }, + "files": [ + "dist", + "README.md", + "package.json" + ], + "keywords": [ + "e2b", + "acp", + "agent-client-protocol", + "ai-agents", + "coding-agents", + "sandbox" + ], + "engines": { + "node": ">=22.19.0" + }, + "browserslist": [ + "defaults" + ], + "dependencies": { + "e2b": "workspace:^" + }, + "peerDependencies": { + "@agentclientprotocol/sdk": "^1.8.0" + } +} diff --git a/packages/acp-js/src/utils.ts b/packages/acp-js/src/utils.ts new file mode 100644 index 0000000000..28cf86e7e3 --- /dev/null +++ b/packages/acp-js/src/utils.ts @@ -0,0 +1,134 @@ +import { InvalidArgumentError, SandboxError } from 'e2b' + +/** + * Default maximum size of a single message from the agent, matching the + * TypeScript ACP SDK. + */ +export const DEFAULT_MAX_MESSAGE_BYTES = 32 * 1024 * 1024 + +const encoder = new TextEncoder() + +/** + * Splits newline-delimited text chunks into complete lines. + */ +export class LineSplitter { + private pending = '' + + constructor(private readonly maxLineBytes: number) { + if (!Number.isSafeInteger(maxLineBytes) || maxLineBytes <= 0) { + throw new InvalidArgumentError( + `maxMessageBytes must be a positive integer, got ${maxLineBytes}` + ) + } + } + + push(chunk: string): string[] { + const parts = (this.pending + chunk).split('\n') + this.pending = parts.pop() ?? '' + // Characters never outnumber UTF-8 bytes, so the cheap length check only + // lets through lines that might be too large. + for (const line of [...parts, this.pending]) { + if ( + line.length > this.maxLineBytes / 4 && + encoder.encode(line).length > this.maxLineBytes + ) { + throw new SandboxError( + `ACP message exceeds the limit of ${this.maxLineBytes} bytes` + ) + } + } + return parts + } + + flush(): string { + const rest = this.pending + this.pending = '' + return rest + } +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +/** + * Methods that would make the agent read files or run commands on the + * client's machine instead of inside the sandbox. + */ +export function isClientExecutionMethod(method: unknown): boolean { + return ( + typeof method === 'string' && + (method.startsWith('fs/') || method.startsWith('terminal/')) + ) +} + +/** + * Turns off the client filesystem and terminal capabilities in an + * `initialize` request, also inside a batch, so the agent keeps using its own + * tools in the sandbox. Other messages are returned unchanged. + */ +export function restrictClientCapabilities(message: T): T { + if (Array.isArray(message)) { + return message.map(restrictClientCapabilities) as T + } + if (!isRecord(message) || message.method !== 'initialize') { + return message + } + const params = isRecord(message.params) ? message.params : {} + const capabilities = isRecord(params.clientCapabilities) + ? params.clientCapabilities + : {} + return { + ...message, + params: { + ...params, + clientCapabilities: { + ...capabilities, + fs: { readTextFile: false, writeTextFile: false }, + terminal: false, + }, + }, + } +} + +/** + * Splits agent output into what is forwarded to the client and the error + * responses sent straight back to the agent for client-execution requests. + */ +export function filterAgentMessage(message: unknown): { + forward?: unknown + replies: unknown[] +} { + const items = Array.isArray(message) ? message : [message] + const forward: unknown[] = [] + const replies: unknown[] = [] + + for (const item of items) { + if (!isRecord(item) || !isClientExecutionMethod(item.method)) { + forward.push(item) + continue + } + // Notifications get no reply; requests are rejected. + if ('id' in item) { + replies.push({ + jsonrpc: '2.0', + id: item.id, + error: { + code: -32601, + message: `Method not found: ${item.method} is not available, the agent runs inside an E2B sandbox`, + }, + }) + } + } + + if (!Array.isArray(message)) { + return { forward: forward[0], replies } + } + return { forward: forward.length ? forward : undefined, replies } +} + +export const PARSE_ERROR_RESPONSE = { + jsonrpc: '2.0', + id: null, + error: { code: -32700, message: 'Parse error' }, +} diff --git a/packages/acp-js/tests/utils.test.ts b/packages/acp-js/tests/utils.test.ts new file mode 100644 index 0000000000..827f99faf3 --- /dev/null +++ b/packages/acp-js/tests/utils.test.ts @@ -0,0 +1,134 @@ +import { assert, expect, test } from 'vitest' +import { + filterAgentMessage, + LineSplitter, + restrictClientCapabilities, +} from '../src/utils' + +test('line splitter joins chunks split mid-line', () => { + const lines = new LineSplitter(1024) + + assert.deepEqual(lines.push('{"a":'), []) + assert.deepEqual(lines.push('1}\n{"b"'), ['{"a":1}']) + assert.deepEqual(lines.push(':2}\n\n'), ['{"b":2}', '']) + assert.equal(lines.flush(), '') +}) + +test('line splitter keeps the trailing partial line for flush', () => { + const lines = new LineSplitter(1024) + + assert.deepEqual(lines.push('{"a":1}\n{"b":2}'), ['{"a":1}']) + assert.equal(lines.flush(), '{"b":2}') +}) + +test('line splitter rejects messages over the limit in bytes', () => { + const lines = new LineSplitter(8) + + assert.deepEqual(lines.push('1234567\n'), ['1234567']) + // Four 3-byte characters are 12 bytes. + expect(() => lines.push('€€€€')).toThrow(/exceeds the limit of 8 bytes/) +}) + +test('initialize turns off client filesystem and terminal', () => { + const message = { + jsonrpc: '2.0', + id: 0, + method: 'initialize', + params: { + protocolVersion: 1, + clientCapabilities: { + fs: { readTextFile: true, writeTextFile: true }, + terminal: true, + _meta: { custom: true }, + }, + }, + } + + const restricted = restrictClientCapabilities(message) + + assert.deepEqual(restricted.params, { + protocolVersion: 1, + clientCapabilities: { + fs: { readTextFile: false, writeTextFile: false }, + terminal: false, + _meta: { custom: true }, + }, + }) + assert.isTrue(message.params.clientCapabilities.terminal) +}) + +test('other client messages pass unchanged', () => { + const message = { jsonrpc: '2.0', id: 1, method: 'session/new', params: {} } + + assert.strictEqual(restrictClientCapabilities(message), message) +}) + +test('agent requests for client fs and terminal are rejected', () => { + const { forward, replies } = filterAgentMessage({ + jsonrpc: '2.0', + id: 7, + method: 'fs/write_text_file', + params: { path: '/tmp/x', content: '' }, + }) + + assert.isUndefined(forward) + assert.equal(replies.length, 1) + assert.deepInclude(replies[0], { jsonrpc: '2.0', id: 7 }) + assert.equal((replies[0] as { error: { code: number } }).error.code, -32601) +}) + +test('agent notifications for client terminal are dropped silently', () => { + const { forward, replies } = filterAgentMessage({ + jsonrpc: '2.0', + method: 'terminal/release', + }) + + assert.isUndefined(forward) + assert.deepEqual(replies, []) +}) + +test('batches keep only forwardable messages', () => { + const update = { jsonrpc: '2.0', method: 'session/update', params: {} } + const { forward, replies } = filterAgentMessage([ + update, + { jsonrpc: '2.0', id: 'x', method: 'terminal/create', params: {} }, + ]) + + assert.deepEqual(forward, [update]) + assert.equal(replies.length, 1) +}) + +test('other agent messages are forwarded', () => { + const message = { + jsonrpc: '2.0', + id: 3, + method: 'session/request_permission', + } + + assert.deepEqual(filterAgentMessage(message), { + forward: message, + replies: [], + }) +}) + +test('initialize inside a batch is restricted too', () => { + const [restricted] = restrictClientCapabilities([ + { + jsonrpc: '2.0', + id: 0, + method: 'initialize', + params: { clientCapabilities: { terminal: true } }, + }, + ]) as { params: { clientCapabilities: unknown } }[] + + assert.deepEqual(restricted.params.clientCapabilities, { + fs: { readTextFile: false, writeTextFile: false }, + terminal: false, + }) +}) + +test('message limit must be a positive integer', () => { + for (const limit of [0, -1, 1.5, Number.NaN, Number.POSITIVE_INFINITY]) { + expect(() => new LineSplitter(limit)).toThrow(/positive integer/) + } +}) diff --git a/packages/acp-js/tsconfig.json b/packages/acp-js/tsconfig.json new file mode 100644 index 0000000000..ff09139d65 --- /dev/null +++ b/packages/acp-js/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.sdk.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src"], + "exclude": ["dist", "node_modules", "tsdown.config.ts"] +} diff --git a/packages/acp-js/tsdown.config.ts b/packages/acp-js/tsdown.config.ts new file mode 100644 index 0000000000..3a056f8886 --- /dev/null +++ b/packages/acp-js/tsdown.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from 'tsdown' + +import { createSdkTsdownConfig } from '../../tsdown.sdk.config.mts' + +export default defineConfig(createSdkTsdownConfig()) diff --git a/packages/acp-js/vitest.config.mts b/packages/acp-js/vitest.config.mts new file mode 100644 index 0000000000..7189b75139 --- /dev/null +++ b/packages/acp-js/vitest.config.mts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vitest/config' +import { config } from 'dotenv' + +import { createSdkVitestConfig } from '../../vitest.sdk.config.mts' + +const env = config() + +export default defineConfig( + createSdkVitestConfig( + { + ...(process.env as Record), + ...env.parsed, + }, + { testTimeout: 90_000 } + ) +) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2d78ccdeb..dcf0682b65 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -95,6 +95,34 @@ importers: specifier: 'catalog:' version: 4.1.11(@types/node@22.20.5)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(msw@2.15.0(@types/node@22.20.5)(@typescript/typescript6@6.0.2))(vite@6.4.3(@types/node@22.20.5)(jiti@2.7.0)(terser@5.46.0)(tsx@4.23.12)(yaml@2.9.1)) + packages/acp-js: + dependencies: + e2b: + specifier: workspace:^ + version: link:../js-sdk + devDependencies: + '@agentclientprotocol/sdk': + specifier: 1.8.0 + version: 1.8.0(zod@4.6.5) + '@types/node': + specifier: 'catalog:' + version: 22.20.5 + '@typescript/native': + specifier: 'catalog:' + version: typescript@7.0.2 + dotenv: + specifier: ^16.4.5 + version: 16.6.1 + tsdown: + specifier: 'catalog:' + version: 0.22.14(@typescript/typescript6@6.0.2)(oxc-resolver@11.24.2)(tsx@4.23.12) + typescript: + specifier: 'catalog:' + version: '@typescript/typescript6@6.0.2' + vitest: + specifier: 'catalog:' + version: 4.1.11(@types/node@22.20.5)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(msw@2.15.0(@types/node@22.20.5)(@typescript/typescript6@6.0.2))(vite@6.4.3(@types/node@22.20.5)(jiti@2.7.0)(terser@5.46.0)(tsx@4.23.12)(yaml@2.9.1)) + packages/cli: dependencies: '@iarna/toml': @@ -369,6 +397,11 @@ importers: packages: + '@agentclientprotocol/sdk@1.8.0': + resolution: {integrity: sha512-fbP7qHZsFyTJVT0/RPrcWJ8An26qLXHEuGCHHEKDyxD1xpXV+Ejyh2LAE6vBAFePkpVjZeeohnbgvuEhZ7wLMg==} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + '@apidevtools/json-schema-ref-parser@11.9.3': resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==} engines: {node: '>= 16'} @@ -4511,6 +4544,10 @@ packages: snapshots: + '@agentclientprotocol/sdk@1.8.0(zod@4.6.5)': + dependencies: + zod: 4.6.5 + '@apidevtools/json-schema-ref-parser@11.9.3': dependencies: '@jsdevtools/ono': 7.1.3 From 2dc86db6ce4bb07985a5205c25dba03d5b682115 Mon Sep 17 00:00:00 2001 From: Maksim Obukhov Date: Thu, 15 Oct 2026 15:11:29 +0200 Subject: [PATCH 04/26] feat(acp-js): add connectAcpAgent connectAcpAgent(sandbox, { command, client }) starts the agent over a streaming command and returns an AcpAgent with the ACP ClientSideConnection, close(), wait() and kill(). Writes, transport replies and EOF share one queue. close() bounds the whole shutdown from the call, kills an agent whose output stream failed without a known exit, and stops delivering later output. Closing the writable is a graceful EOF that still reports a failed exit and a failure to close stdin. --- packages/acp-js/README.md | 70 ++++ packages/acp-js/src/index.ts | 126 +++++++ packages/acp-js/src/transport.ts | 354 ++++++++++++++++++ .../acp-js/tests/transport.offline.test.ts | 144 +++++++ 4 files changed, 694 insertions(+) create mode 100644 packages/acp-js/README.md create mode 100644 packages/acp-js/src/index.ts create mode 100644 packages/acp-js/src/transport.ts create mode 100644 packages/acp-js/tests/transport.offline.test.ts diff --git a/packages/acp-js/README.md b/packages/acp-js/README.md new file mode 100644 index 0000000000..91a8c3e04e --- /dev/null +++ b/packages/acp-js/README.md @@ -0,0 +1,70 @@ +# E2B ACP + +Run [Agent Client Protocol](https://agentclientprotocol.com) (ACP) agents such as goose, OpenCode or Claude Code inside an [E2B](https://e2b.dev) sandbox, and drive them from your code with the official ACP client. + +Source: [packages/acp-js](https://github.com/e2b-dev/E2B/tree/main/packages/acp-js) + +## Install + +``` +npm i e2b @e2b/acp @agentclientprotocol/sdk +``` + +Set `E2B_API_KEY` to your [E2B API key](https://e2b.dev/dashboard?tab=keys), and the provider key the agent needs. + +## Usage + +Create a sandbox from a template with the agent installed, then start the agent and connect your ACP client to it: + +```ts +import { Sandbox } from 'e2b' +import { connectAcpAgent } from '@e2b/acp' +import { PROTOCOL_VERSION, type Client } from '@agentclientprotocol/sdk' + +const client: Client = { + async requestPermission({ options }) { + const allow = options.find((o) => o.kind === 'allow_once') ?? options[0] + return { outcome: { outcome: 'selected', optionId: allow.optionId } } + }, + async sessionUpdate({ update }) { + if (update.sessionUpdate === 'agent_message_chunk' && update.content.type === 'text') { + process.stdout.write(update.content.text) + } + }, +} + +const sandbox = await Sandbox.create('goose', { timeoutMs: 10 * 60_000 }) +try { + const agent = await connectAcpAgent(sandbox, { + command: 'goose acp --with-builtin developer', + client, + envs: { + GOOSE_PROVIDER: 'openai', + GOOSE_MODEL: 'gpt-5.6-luna', + OPENAI_API_KEY: process.env.OPENAI_API_KEY!, + }, + }) + + await agent.connection.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} }) + const { sessionId } = await agent.connection.newSession({ cwd: '/home/user', mcpServers: [] }) + await agent.connection.prompt({ + sessionId, + prompt: [{ type: 'text', text: 'Create fizzbuzz.py for 1 to 15 and run it.' }], + }) + + await agent.close() +} finally { + await sandbox.kill() +} +``` + +## Where the agent's tools run + +The agent talks ACP over its stdin and stdout. `connectAcpAgent` turns off the client filesystem and terminal capabilities in `initialize` and answers agent requests for `fs/*` or `terminal/*` with an error, so the agent edits files and runs commands with its own tools inside the sandbox. It does not restrict the agent's network access or the MCP servers you pass to it. + +## Lifecycle + +- `agent.close()` sends EOF to the agent and kills it if it has not exited within `timeoutMs` (5 seconds by default) of the call. Messages the agent sends after that are not delivered. +- `agent.wait()` resolves with the exit code once the agent exits. +- If the agent exits with an error, the connection fails with the exit code and the end of the agent's stderr. Pass `onStderr` to receive its logs. +- The agent runs as long as the sandbox does, so set the sandbox timeout for the longest task you expect. diff --git a/packages/acp-js/src/index.ts b/packages/acp-js/src/index.ts new file mode 100644 index 0000000000..fad72db78a --- /dev/null +++ b/packages/acp-js/src/index.ts @@ -0,0 +1,126 @@ +import { + ClientSideConnection, + type Agent, + type Client, +} from '@agentclientprotocol/sdk' +import type { Sandbox } from 'e2b' +import { + startAcpTransport, + type AcpAgentExit, + type AcpCloseOpts, + type AcpStartOpts, + type AcpTransport, +} from './transport' + +export type { AcpAgentExit, AcpCloseOpts, AcpStartOpts } from './transport' + +/** + * Options for connecting to an ACP agent in a sandbox. + */ +export interface ConnectAcpAgentOpts extends AcpStartOpts { + /** + * Command that starts the agent in ACP mode, for example `goose acp`. + */ + command: string + /** + * Your ACP client, or a function that creates it for the agent connection. + * It answers permission requests and receives the agent's session updates. + */ + client: Client | ((agent: Agent) => Client) +} + +/** + * ACP agent running in a sandbox, connected to your client. + */ +export class AcpAgent { + /** + * @hidden + * @internal + */ + constructor( + private readonly transport: AcpTransport, + /** + * Connection to the agent. Use it to initialize, create sessions and send prompts. + */ + readonly connection: ClientSideConnection + ) {} + + /** + * Process ID of the agent in the sandbox. + */ + get pid(): number { + return this.transport.pid + } + + /** + * Wait for the agent process to exit. + * + * It rejects if the agent's output stream failed before its exit status was known. + * + * @returns exit status of the agent. + */ + async wait(): Promise { + return this.transport.wait() + } + + /** + * Close the agent: send EOF to its stdin and kill it if it has not exited + * within `timeoutMs` of this call. Messages the agent sends after this call + * are not delivered. + * + * @param opts options for closing the agent. + */ + async close(opts?: AcpCloseOpts): Promise { + return this.transport.close(opts) + } + + /** + * Kill the agent process with `SIGKILL`. + * + * @returns `true` if the agent was killed, `false` if it was not found. + */ + async kill(): Promise { + return this.transport.kill() + } +} + +/** + * Start an [Agent Client Protocol](https://agentclientprotocol.com) agent in + * the sandbox and connect your ACP client to it over the agent's stdin/stdout. + * + * The client filesystem and terminal capabilities are turned off in + * `initialize`, and agent requests for `fs/*` or `terminal/*` are answered + * with an error without reaching your client, so the agent edits files and + * runs commands with its own tools in the sandbox. Network access and MCP + * servers you pass to the agent are not restricted. + * + * @param sandbox sandbox with the agent installed. + * @param opts options for starting and connecting to the agent. + * + * @returns `AcpAgent` with the ACP connection. + * + * @example + * ```ts + * import { Sandbox } from 'e2b' + * import { connectAcpAgent } from '@e2b/acp' + * import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk' + * + * const sandbox = await Sandbox.create('goose') + * const agent = await connectAcpAgent(sandbox, { command: 'goose acp', client }) + * await agent.connection.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} }) + * // ... + * await agent.close() + * ``` + */ +export async function connectAcpAgent( + sandbox: Sandbox, + opts: ConnectAcpAgentOpts +): Promise { + const { command, client, ...startOpts } = opts + const transport = await startAcpTransport(sandbox, command, startOpts) + const toClient = typeof client === 'function' ? client : () => client + return new AcpAgent( + transport, + new ClientSideConnection(toClient, transport.stream) + ) +} diff --git a/packages/acp-js/src/transport.ts b/packages/acp-js/src/transport.ts new file mode 100644 index 0000000000..0983bf6d0e --- /dev/null +++ b/packages/acp-js/src/transport.ts @@ -0,0 +1,354 @@ +import type { AnyMessage } from '@agentclientprotocol/sdk' +import { + CommandExitError, + SandboxError, + type CommandHandle, + type CommandStartOpts, + type Sandbox, +} from 'e2b' +import { + DEFAULT_MAX_MESSAGE_BYTES, + filterAgentMessage, + LineSplitter, + PARSE_ERROR_RESPONSE, + restrictClientCapabilities, +} from './utils' + +/** + * Options for starting an ACP agent in a sandbox. + */ +export interface AcpStartOpts extends Pick< + CommandStartOpts, + 'cwd' | 'envs' | 'user' | 'requestTimeoutMs' +> { + /** + * Callback for the agent's stderr output (logs). The agent's stdout is reserved for the protocol. + */ + onStderr?: (data: string) => void | Promise + /** + * Maximum size of a single message from the agent in **bytes**. + * + * @default 33_554_432 // 32 MiB + */ + maxMessageBytes?: number +} + +/** + * Options for closing an ACP agent. + */ +export interface AcpCloseOpts { + /** + * Time for the agent to exit after `close()` is called, in **milliseconds**. + * It covers sending EOF to the agent's stdin; after it the agent is killed. + * + * @default 5_000 // 5 seconds + */ + timeoutMs?: number +} + +/** + * Exit status of an ACP agent process. + */ +export interface AcpAgentExit { + /** + * Exit code of the agent process. + */ + exitCode: number + /** + * Error message if the process failed. + */ + error?: string +} + +/** + * Message stream connected to an ACP agent. + */ +export interface AcpStream { + readable: ReadableStream + writable: WritableStream +} + +/** + * The part of `Sandbox` the transport needs. + */ +export type AcpSandbox = Pick + +const STDERR_TAIL_CHARS = 4096 +const READ_QUEUE_MESSAGES = 64 + +/** + * ACP over the stdin/stdout of an agent process in the sandbox. + */ +export class AcpTransport { + /** + * @hidden + * @internal + */ + constructor( + readonly pid: number, + readonly stream: AcpStream, + private readonly handle: CommandHandle, + private readonly exit: Promise, + private readonly exitStatus: () => AcpAgentExit | undefined, + private readonly stop: () => Promise + ) {} + + /** + * Wait for the agent process to exit. + * + * It rejects if the agent's output stream failed before its exit status was known. + */ + async wait(): Promise { + return this.exit + } + + /** + * Close the agent: send EOF to its stdin and kill it if it has not exited + * within `timeoutMs` of this call. Messages the agent sends after this call + * are not delivered. + */ + async close(opts?: AcpCloseOpts): Promise { + // Only a known exit status means the process is gone. A failed output + // stream can leave it running. + if (this.exitStatus()) { + return + } + + let timer: ReturnType | undefined + const exited = await Promise.race([ + this.stop() + .then(() => this.exit) + .then( + () => true, + () => false + ), + new Promise((resolve) => { + timer = setTimeout(() => resolve(false), opts?.timeoutMs ?? 5_000) + }), + ]) + clearTimeout(timer) + + if (!exited && !this.exitStatus()) { + await this.kill().catch(() => false) + } + } + + /** + * Kill the agent process with `SIGKILL`. + * + * @returns `true` if the agent was killed, `false` if it was not found. + */ + async kill(): Promise { + return this.handle.kill() + } +} + +/** + * Start an ACP agent in the sandbox and connect a message stream to its stdin/stdout. + * + * The client filesystem and terminal capabilities are turned off in + * `initialize`, and agent requests for `fs/*` or `terminal/*` are answered + * with an error without reaching the client, so the agent edits files and + * runs commands with its own tools in the sandbox. + */ +export async function startAcpTransport( + sandbox: AcpSandbox, + cmd: string, + opts?: AcpStartOpts +): Promise { + const commands = sandbox.commands + const lines = new LineSplitter( + opts?.maxMessageBytes ?? DEFAULT_MAX_MESSAGE_BYTES + ) + let stderrTail = '' + let ended = false + // Set by close(): the exit that follows is expected, and output after it is + // not delivered. + let closing = false + let resumeReading: (() => void) | undefined + let controller!: ReadableStreamDefaultController + let exitStatus: AcpAgentExit | undefined + + let resolveHandle!: (handle: CommandHandle) => void + let rejectHandle!: (err: unknown) => void + const handleReady = new Promise((resolve, reject) => { + resolveHandle = resolve + rejectHandle = reject + }) + handleReady.catch(() => {}) + + // Client messages, replies to the agent and EOF share one ordered write + // path, so concurrent writes never interleave. + let writes: Promise = Promise.resolve() + const enqueueWrite = (write: (handle: CommandHandle) => Promise) => { + const result = writes.then(async () => { + await write(await handleReady) + }) + writes = result.catch(() => {}) + return result + } + const send = (message: unknown) => { + const line = JSON.stringify(message) + '\n' + return enqueueWrite((handle) => handle.sendStdin(line)) + } + const endInput = () => + enqueueWrite((handle) => + commands.supportsStdinClose ? handle.closeStdin() : handle.kill() + ) + + const end = (err?: unknown) => { + if (ended) { + return + } + ended = true + resumeReading?.() + if (err === undefined) { + controller.close() + } else { + controller.error(err) + } + } + + const deliver = async (line: string) => { + const text = line.trim() + if (!text || ended || closing) { + return + } + + let message: unknown + try { + message = JSON.parse(text) + } catch { + send(PARSE_ERROR_RESPONSE).catch(() => {}) + return + } + + const { forward, replies } = filterAgentMessage(message) + for (const reply of replies) { + send(reply).catch(() => {}) + } + if (forward === undefined) { + return + } + + controller.enqueue(forward as AnyMessage) + // Stop reading agent output until the client catches up. + if ((controller.desiredSize ?? 1) <= 0 && !ended && !closing) { + await new Promise((resolve) => (resumeReading = resolve)) + } + } + + let transport: AcpTransport | undefined + const closeTransport = () => transport?.close() ?? Promise.resolve() + + const readable = new ReadableStream( + { + start: (c) => { + controller = c + }, + pull: () => { + resumeReading?.() + resumeReading = undefined + }, + cancel: async () => { + ended = true + resumeReading?.() + await closeTransport() + }, + }, + new CountQueuingStrategy({ highWaterMark: READ_QUEUE_MESSAGES }) + ) + + const writable = new WritableStream({ + write: (message) => send(restrictClientCapabilities(message)), + // Closing the writable is a graceful EOF: the agent's remaining messages + // and a failed exit still reach the client. + close: () => endInput(), + abort: () => closeTransport(), + }) + + const stop = () => { + closing = true + resumeReading?.() + resumeReading = undefined + return endInput() + } + + let handle: CommandHandle + try { + handle = await commands.startStreaming(cmd, { + stdin: true, + timeoutMs: 0, + cwd: opts?.cwd, + envs: opts?.envs, + user: opts?.user, + requestTimeoutMs: opts?.requestTimeoutMs, + onStdout: async (data) => { + if (ended || closing) { + return + } + let complete: string[] + try { + complete = lines.push(data) + } catch (err) { + end(err) + void closeTransport() + return + } + for (const line of complete) { + await deliver(line) + } + }, + onStderr: async (data) => { + stderrTail = (stderrTail + data).slice(-STDERR_TAIL_CHARS) + await opts?.onStderr?.(data) + }, + }) + } catch (err) { + rejectHandle(err) + throw err + } + resolveHandle(handle) + + const exit = handle.wait().then( + (result): AcpAgentExit => ({ + exitCode: result.exitCode, + error: result.error, + }), + (err): AcpAgentExit => { + if (err instanceof CommandExitError) { + return { exitCode: err.exitCode, error: err.error } + } + throw err + } + ) + + exit.then( + async (status) => { + exitStatus = status + await deliver(lines.flush()) + if (status.exitCode === 0 || closing) { + end() + return + } + end( + new SandboxError( + `ACP agent exited with code ${status.exitCode}${ + status.error ? `: ${status.error}` : '' + }${stderrTail ? `\n${stderrTail}` : ''}` + ) + ) + }, + (err) => end(err) + ) + + transport = new AcpTransport( + handle.pid, + { readable, writable }, + handle, + exit, + () => exitStatus, + stop + ) + return transport +} diff --git a/packages/acp-js/tests/transport.offline.test.ts b/packages/acp-js/tests/transport.offline.test.ts new file mode 100644 index 0000000000..3a691a61a9 --- /dev/null +++ b/packages/acp-js/tests/transport.offline.test.ts @@ -0,0 +1,144 @@ +import { CommandExitError, type CommandStartOpts } from 'e2b' +import { assert, expect, test, vi } from 'vitest' +import { startAcpTransport, type AcpSandbox } from '../src/transport' + +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)) + +// A command handle driven by the test instead of a sandbox. +function fakeAgent(opts: { closeStdin?: () => Promise } = {}) { + let callbacks!: Pick + let settle!: { resolve: (r: unknown) => void; reject: (e: unknown) => void } + const exit = new Promise((resolve, reject) => (settle = { resolve, reject })) + exit.catch(() => {}) + + const writes: string[] = [] + let active = 0 + let maxActive = 0 + const exitWith = (exitCode: number) => { + const result = { exitCode, stdout: '', stderr: '' } + if (exitCode === 0) settle.resolve(result) + else settle.reject(new CommandExitError(result)) + } + const handle = { + pid: 42, + sendStdin: async (data: string) => { + active++ + maxActive = Math.max(maxActive, active) + await sleep(2) + writes.push(data) + active-- + }, + closeStdin: opts.closeStdin ?? (async () => {}), + kill: vi.fn(async () => { + exitWith(-1) + return true + }), + wait: () => exit, + } + const sandbox = { + commands: { + supportsStdinClose: true, + startStreaming: async (_cmd: string, o: CommandStartOpts) => { + callbacks = o + return handle + }, + }, + } as unknown as AcpSandbox + + return { + sandbox, + handle, + writes, + maxActive: () => maxActive, + emit: (message: unknown) => + callbacks.onStdout!(JSON.stringify(message) + '\n'), + exitWith, + fail: (err: unknown) => settle.reject(err), + } +} + +test('client writes and transport replies never overlap', async () => { + const agent = fakeAgent() + const transport = await startAcpTransport(agent.sandbox, 'agent') + const writer = transport.stream.writable.getWriter() + + const pending = [] + for (let i = 0; i < 10; i++) { + pending.push(writer.write({ jsonrpc: '2.0', id: i, method: 'echo' })) + // Each forbidden request makes the transport write a reply itself. + pending.push( + agent.emit({ jsonrpc: '2.0', id: `fs-${i}`, method: 'fs/read_text_file' }) + ) + } + await Promise.all(pending) + await writer.ready + await sleep(50) + + assert.equal(agent.writes.length, 20) + assert.equal(agent.maxActive(), 1) +}) + +test('graceful EOF still reports a failed exit', async () => { + const agent = fakeAgent() + const transport = await startAcpTransport(agent.sandbox, 'agent') + const reader = transport.stream.readable.getReader() + + await transport.stream.writable.getWriter().close() + agent.exitWith(3) + + await expect(reader.read()).rejects.toThrow(/exited with code 3/) +}) + +test('graceful EOF surfaces a failure to close stdin', async () => { + const agent = fakeAgent({ + closeStdin: async () => { + throw new Error('stdin gone') + }, + }) + const transport = await startAcpTransport(agent.sandbox, 'agent') + + await expect(transport.stream.writable.getWriter().close()).rejects.toThrow( + 'stdin gone' + ) +}) + +test('close kills the agent when its output stream failed', async () => { + // EOF goes through, but the process is still running. + const agent = fakeAgent() + const transport = await startAcpTransport(agent.sandbox, 'agent') + + agent.fail(new Error('output stream broke')) + await expect(transport.wait()).rejects.toThrow('output stream broke') + await transport.close({ timeoutMs: 50 }) + + expect(agent.handle.kill).toHaveBeenCalled() +}) + +test('close timeout covers a hung EOF and then kills', async () => { + const agent = fakeAgent({ closeStdin: () => new Promise(() => {}) }) + const transport = await startAcpTransport(agent.sandbox, 'agent') + + const started = Date.now() + await transport.close({ timeoutMs: 50 }) + + assert.isBelow(Date.now() - started, 1_000) + expect(agent.handle.kill).toHaveBeenCalledOnce() +}) + +test('close keeps buffered messages and drops later ones', async () => { + const agent = fakeAgent() + const transport = await startAcpTransport(agent.sandbox, 'agent') + const reader = transport.stream.readable.getReader() + + await agent.emit({ jsonrpc: '2.0', method: 'before' }) + const closing = transport.close({ timeoutMs: 50 }) + await agent.emit({ jsonrpc: '2.0', method: 'after' }) + agent.exitWith(0) + await closing + + assert.equal( + ((await reader.read()).value as { method: string }).method, + 'before' + ) + assert.isTrue((await reader.read()).done) +}) From fd2fca61dc3acb0bc9ec123619c972292fd76e33 Mon Sep 17 00:00:00 2001 From: Maksim Obukhov Date: Thu, 15 Oct 2026 15:11:29 +0200 Subject: [PATCH 05/26] test(acp-js): cover connectAcpAgent against a scripted agent A small Python agent in a sandbox exercises the capability rewrite, rejected fs/terminal calls, ordering, UTF-8 split across bytes, the parse-error reply, the size limit, EOF, kill on close, failed exits, a failing stderr callback and cancellation, without any model or key. --- packages/acp-js/tests/connect.test.ts | 29 +++ packages/acp-js/tests/fakeAgent.ts | 74 +++++++ packages/acp-js/tests/setup.ts | 14 ++ packages/acp-js/tests/transport.test.ts | 258 ++++++++++++++++++++++++ 4 files changed, 375 insertions(+) create mode 100644 packages/acp-js/tests/connect.test.ts create mode 100644 packages/acp-js/tests/fakeAgent.ts create mode 100644 packages/acp-js/tests/setup.ts create mode 100644 packages/acp-js/tests/transport.test.ts diff --git a/packages/acp-js/tests/connect.test.ts b/packages/acp-js/tests/connect.test.ts new file mode 100644 index 0000000000..60b11c513e --- /dev/null +++ b/packages/acp-js/tests/connect.test.ts @@ -0,0 +1,29 @@ +import { PROTOCOL_VERSION, type Client } from '@agentclientprotocol/sdk' +import { assert } from 'vitest' +import { connectAcpAgent } from '../src' +import { FAKE_AGENT_CMD, uploadFakeAgent } from './fakeAgent' +import { sandboxTest } from './setup' + +const client: Client = { + async requestPermission() { + return { outcome: { outcome: 'cancelled' } } + }, + async sessionUpdate() {}, +} + +sandboxTest('connectAcpAgent connects an ACP client', async ({ sandbox }) => { + await uploadFakeAgent(sandbox) + const agent = await connectAcpAgent(sandbox, { + command: FAKE_AGENT_CMD, + client, + }) + + const response = await agent.connection.initialize({ + protocolVersion: PROTOCOL_VERSION, + clientCapabilities: { fs: { readTextFile: true }, terminal: true }, + }) + + assert.equal(response.protocolVersion, 1) + await agent.close() + assert.equal((await agent.wait()).exitCode, 0) +}) diff --git a/packages/acp-js/tests/fakeAgent.ts b/packages/acp-js/tests/fakeAgent.ts new file mode 100644 index 0000000000..d3393ca036 --- /dev/null +++ b/packages/acp-js/tests/fakeAgent.ts @@ -0,0 +1,74 @@ +import type { Sandbox } from 'e2b' + +// Minimal deterministic ACP-shaped agent driven by test methods. +const FAKE_AGENT = String.raw` +import json, os, sys, time + +def send(message): + sys.stdout.write(json.dumps(message, ensure_ascii=False) + "\n") + sys.stdout.flush() + +pending = {} +errors = [] +for line in sys.stdin: + message = json.loads(line) + method, id, params = message.get("method"), message.get("id"), message.get("params") + if method is None and message.get("id") is None: + # Error response without an id, such as a parse error. + errors.append(message.get("error")) + continue + if method is None: + # Response to one of our requests. + origin = pending.pop(message["id"], None) + if origin is not None: + send({"jsonrpc": "2.0", "id": origin, "result": message}) + continue + if method == "initialize": + send({"jsonrpc": "2.0", "id": id, "result": {"protocolVersion": 1, "received": params}}) + elif method == "test/echo": + send({"jsonrpc": "2.0", "id": id, "result": params}) + elif method == "test/client_call": + # Ask the client to run something on its machine. + pending["call-" + str(id)] = id + send({"jsonrpc": "2.0", "id": "call-" + str(id), "method": params["method"], "params": {}}) + elif method == "test/notify_client": + send({"jsonrpc": "2.0", "method": "terminal/output", "params": {}}) + send({"jsonrpc": "2.0", "id": id, "result": "sent"}) + elif method == "test/bytewise": + data = (json.dumps({"jsonrpc": "2.0", "id": id, "result": params["text"]}, ensure_ascii=False) + "\n").encode() + for b in data: + os.write(1, bytes([b])) + time.sleep(0.002) + elif method == "test/garbage": + sys.stdout.write("not json\n") + sys.stdout.flush() + send({"jsonrpc": "2.0", "id": id, "result": "garbage sent"}) + elif method == "test/errors": + send({"jsonrpc": "2.0", "id": id, "result": errors}) + elif method == "test/big": + send({"jsonrpc": "2.0", "id": id, "result": "x" * params["size"]}) + elif method == "test/stderr_then_sleep": + sys.stderr.write("log line\n") + sys.stderr.flush() + send({"jsonrpc": "2.0", "id": id, "result": "sleeping"}) + time.sleep(600) + elif method == "test/exit": + sys.stderr.write(params.get("stderr", "")) + sys.stderr.flush() + sys.exit(params["code"]) + elif method == "test/flood": + for i in range(params["count"]): + send({"jsonrpc": "2.0", "method": "session/update", "params": {"i": i}}) + send({"jsonrpc": "2.0", "id": id, "result": "flooded"}) + elif method == "test/ignore_eof": + os.close(0) + send({"jsonrpc": "2.0", "id": id, "result": "ignoring"}) + time.sleep(600) +` + +export const FAKE_AGENT_PATH = '/tmp/fake_acp_agent.py' +export const FAKE_AGENT_CMD = `python3 -u ${FAKE_AGENT_PATH}` + +export async function uploadFakeAgent(sandbox: Sandbox) { + await sandbox.files.write(FAKE_AGENT_PATH, FAKE_AGENT) +} diff --git a/packages/acp-js/tests/setup.ts b/packages/acp-js/tests/setup.ts new file mode 100644 index 0000000000..1cdb7a4fc2 --- /dev/null +++ b/packages/acp-js/tests/setup.ts @@ -0,0 +1,14 @@ +import { Sandbox } from 'e2b' +import { test as base } from 'vitest' + +export const sandboxTest = base.extend<{ sandbox: Sandbox }>({ + // eslint-disable-next-line no-empty-pattern + sandbox: async ({}, use) => { + const sandbox = await Sandbox.create({ timeoutMs: 5 * 60_000 }) + try { + await use(sandbox) + } finally { + await sandbox.kill() + } + }, +}) diff --git a/packages/acp-js/tests/transport.test.ts b/packages/acp-js/tests/transport.test.ts new file mode 100644 index 0000000000..dc40dae299 --- /dev/null +++ b/packages/acp-js/tests/transport.test.ts @@ -0,0 +1,258 @@ +import type { AnyMessage } from '@agentclientprotocol/sdk' +import { assert } from 'vitest' +import { startAcpTransport, type AcpStartOpts } from '../src/transport' +import { FAKE_AGENT_CMD, uploadFakeAgent } from './fakeAgent' +import { sandboxTest } from './setup' + +type Message = Record + +async function startAgent( + sandbox: Parameters[1]>[0]['sandbox'], + opts?: AcpStartOpts +) { + await uploadFakeAgent(sandbox) + const agent = await startAcpTransport(sandbox, FAKE_AGENT_CMD, opts) + const writer = agent.stream.writable.getWriter() + const reader = agent.stream.readable.getReader() + const call = async (id: number, method: string, params: unknown = {}) => { + await writer.write({ jsonrpc: '2.0', id, method, params }) + const { value } = await reader.read() + return value as Message + } + return { agent, writer, reader, call } +} + +sandboxTest( + 'initialize reaches the agent without client fs and terminal', + async ({ sandbox }) => { + const { agent, call } = await startAgent(sandbox) + + const response = await call(0, 'initialize', { + protocolVersion: 1, + clientCapabilities: { + fs: { readTextFile: true, writeTextFile: true }, + terminal: true, + }, + }) + + assert.deepEqual(response.result.received.clientCapabilities, { + fs: { readTextFile: false, writeTextFile: false }, + terminal: false, + }) + await agent.close() + } +) + +sandboxTest( + 'agent requests for client fs and terminal are rejected in the transport', + async ({ sandbox }) => { + const { agent, call } = await startAgent(sandbox) + + for (const [id, method] of [ + [1, 'fs/write_text_file'], + [2, 'fs/read_text_file'], + [3, 'terminal/create'], + ] as const) { + // The client only sees the agent's final answer, never the request. + const response = await call(id, 'test/client_call', { method }) + assert.equal(response.id, id) + assert.equal(response.result.error.code, -32601) + } + + // Notifications are dropped; the next message is the response. + const response = await call(4, 'test/notify_client') + assert.equal(response.result, 'sent') + await agent.close() + } +) + +sandboxTest('writes keep their order', async ({ sandbox }) => { + const { agent, writer, reader } = await startAgent(sandbox) + const count = 100 + + await Promise.all( + Array.from({ length: count }, (_, i) => + writer.write({ jsonrpc: '2.0', id: i, method: 'test/echo', params: i }) + ) + ) + + for (let i = 0; i < count; i++) { + const { value } = await reader.read() + assert.equal((value as Message).result, i) + } + await agent.close() +}) + +sandboxTest( + 'messages split into single bytes are reassembled', + async ({ sandbox }) => { + const { agent, call } = await startAgent(sandbox) + const text = 'Привет 🌍 E2B' + + const response = await call(5, 'test/bytewise', { text }) + + assert.equal(response.result, text) + await agent.close() + } +) + +sandboxTest( + 'invalid agent output gets a parse error and the stream continues', + async ({ sandbox }) => { + const { agent, call } = await startAgent(sandbox) + + assert.equal((await call(6, 'test/garbage')).result, 'garbage sent') + + // The agent got a parse error reply, and the stream still works. + const errors = await call(7, 'test/errors') + assert.equal(errors.result[0].code, -32700) + await agent.close() + } +) + +sandboxTest('messages over the limit fail the stream', async ({ sandbox }) => { + const { agent, writer, reader } = await startAgent(sandbox, { + maxMessageBytes: 1024, + }) + + await writer.write({ + jsonrpc: '2.0', + id: 7, + method: 'test/big', + params: { size: 4096 }, + }) + + let error: unknown + try { + await reader.read() + } catch (err) { + error = err + } + assert.match(String(error), /exceeds the limit of 1024 bytes/) + await agent.close() +}) + +sandboxTest( + 'closing the writable sends EOF and ends the stream', + async ({ sandbox }) => { + const { agent, writer, reader } = await startAgent(sandbox) + + await writer.close() + + const { done } = await reader.read() + assert.isTrue(done) + assert.deepEqual(await agent.wait(), { exitCode: 0, error: undefined }) + } +) + +sandboxTest( + 'close kills an agent that ignores EOF', + async ({ sandbox }) => { + const { agent, call, reader } = await startAgent(sandbox) + assert.equal((await call(8, 'test/ignore_eof')).result, 'ignoring') + + const started = Date.now() + await agent.close({ timeoutMs: 1_000 }) + + assert.isBelow(Date.now() - started, 10_000) + const { done } = await reader.read() + assert.isTrue(done) + const processes = await sandbox.commands.list() + assert.isFalse(processes.some((p) => p.pid === agent.pid)) + }, + 30_000 +) + +sandboxTest( + 'agent failure errors the stream with its stderr', + async ({ sandbox }) => { + let stderr = '' + const { agent, writer, reader } = await startAgent(sandbox, { + onStderr: (data) => { + stderr += data + }, + }) + + await writer.write({ + jsonrpc: '2.0', + id: 9, + method: 'test/exit', + params: { code: 3, stderr: 'boom' }, + } as AnyMessage) + + let error: unknown + try { + await reader.read() + } catch (err) { + error = err + } + assert.match(String(error), /exited with code 3[\s\S]*boom/) + assert.equal(stderr, 'boom') + assert.equal((await agent.wait()).exitCode, 3) + } +) + +sandboxTest('cancelling the readable closes the agent', async ({ sandbox }) => { + const { agent, reader } = await startAgent(sandbox) + + await reader.cancel() + + assert.equal((await agent.wait()).exitCode, 0) +}) + +sandboxTest( + 'close does not hang when the client stops reading', + async ({ sandbox }) => { + const { agent, writer } = await startAgent(sandbox) + + // Far more messages than the read queue holds, none of them read. + await writer.write({ + jsonrpc: '2.0', + id: 10, + method: 'test/flood', + params: { count: 1000 }, + }) + await new Promise((resolve) => setTimeout(resolve, 2_000)) + + await agent.close({ timeoutMs: 2_000 }) + + assert.equal((await agent.wait()).exitCode, 0) + }, + 30_000 +) + +sandboxTest( + 'close stops the agent when the stderr callback failed', + async ({ sandbox }) => { + const { agent, writer, reader } = await startAgent(sandbox, { + onStderr: () => { + throw new Error('callback failed') + }, + }) + // The agent logs to stderr and keeps running. + await writer.write({ + jsonrpc: '2.0', + id: 11, + method: 'test/stderr_then_sleep', + params: {}, + }) + + // The failed callback ends the output stream, but not the process. + let error: unknown + try { + await reader.read() + } catch (err) { + error = err + } + assert.match(String(error), /callback failed/) + assert.isTrue( + (await sandbox.commands.list()).some((p) => p.pid === agent.pid) + ) + + await agent.close({ timeoutMs: 1_000 }) + + const processes = await sandbox.commands.list() + assert.isFalse(processes.some((p) => p.pid === agent.pid)) + }, + 30_000 +) From e93ecfa656473f42082d870e3d5449f2d3225894 Mon Sep 17 00:00:00 2001 From: Maksim Obukhov Date: Thu, 15 Oct 2026 15:27:31 +0200 Subject: [PATCH 06/26] feat(acp-python): add e2b-acp package with ACP framing helpers Python counterpart of @e2b/acp: newline-delimited JSON framing with a validated per-message size limit, the initialize capability rewrite (also inside a batch) that turns off client fs and terminal, and method-not-found replies for agent fs/* and terminal/* requests. --- packages/acp-python/LICENSE | 9 + packages/acp-python/Makefile | 13 + packages/acp-python/e2b_acp/utils.py | 151 +++ packages/acp-python/package.json | 17 + packages/acp-python/pyproject.toml | 41 + packages/acp-python/pytest.ini | 6 + packages/acp-python/tests/test_utils.py | 125 +++ packages/acp-python/uv.lock | 1335 +++++++++++++++++++++++ 8 files changed, 1697 insertions(+) create mode 100644 packages/acp-python/LICENSE create mode 100644 packages/acp-python/Makefile create mode 100644 packages/acp-python/e2b_acp/utils.py create mode 100644 packages/acp-python/package.json create mode 100644 packages/acp-python/pyproject.toml create mode 100644 packages/acp-python/pytest.ini create mode 100644 packages/acp-python/tests/test_utils.py create mode 100644 packages/acp-python/uv.lock diff --git a/packages/acp-python/LICENSE b/packages/acp-python/LICENSE new file mode 100644 index 0000000000..4aa6529d10 --- /dev/null +++ b/packages/acp-python/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2025 FOUNDRYLABS, INC. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/acp-python/Makefile b/packages/acp-python/Makefile new file mode 100644 index 0000000000..eba07466b1 --- /dev/null +++ b/packages/acp-python/Makefile @@ -0,0 +1,13 @@ +typecheck: + ty check e2b_acp + +lint: + ruff check . + ruff format --check . + +format: + ruff format . + +.PHONY: test +test: + uv run pytest --verbose --numprocesses=4 diff --git a/packages/acp-python/e2b_acp/utils.py b/packages/acp-python/e2b_acp/utils.py new file mode 100644 index 0000000000..4f16fb080e --- /dev/null +++ b/packages/acp-python/e2b_acp/utils.py @@ -0,0 +1,151 @@ +import json +import math +from typing import Any, Dict, List, Tuple + +from e2b import InvalidArgumentException, SandboxException + +JsonRpcMessage = Dict[str, Any] +""" +JSON-RPC 2.0 message exchanged with an ACP agent. +""" + +DEFAULT_MAX_MESSAGE_BYTES = 32 * 1024 * 1024 +""" +Default maximum size of a single message from the agent, the same as the +TypeScript ACP SDK uses. +""" + +PARSE_ERROR_RESPONSE: JsonRpcMessage = { + "jsonrpc": "2.0", + "id": None, + "error": {"code": -32700, "message": "Parse error"}, +} + + +class LineSplitter: + """ + Splits newline-delimited text chunks into complete lines. + """ + + def __init__(self, max_line_bytes: int): + if ( + isinstance(max_line_bytes, bool) + or not isinstance(max_line_bytes, (int, float)) + or not math.isfinite(max_line_bytes) + or max_line_bytes != int(max_line_bytes) + or max_line_bytes <= 0 + ): + raise InvalidArgumentException( + f"max_message_bytes must be a positive integer, got {max_line_bytes}" + ) + self._max_line_bytes = int(max_line_bytes) + self._pending = "" + + def push(self, chunk: str) -> List[str]: + parts = (self._pending + chunk).split("\n") + self._pending = parts.pop() + # Characters never outnumber UTF-8 bytes, so the cheap length check + # only lets through lines that might be too large. + for line in [*parts, self._pending]: + if ( + len(line) > self._max_line_bytes / 4 + and len(line.encode()) > self._max_line_bytes + ): + raise SandboxException( + f"ACP message exceeds the limit of {self._max_line_bytes} bytes" + ) + return parts + + def flush(self) -> str: + rest, self._pending = self._pending, "" + return rest + + +def encode_message(message: Any) -> str: + return json.dumps(message, separators=(",", ":"), ensure_ascii=False) + "\n" + + +def is_client_execution_method(method: Any) -> bool: + """ + Methods that would make the agent read files or run commands on the + client's machine instead of inside the sandbox. + """ + return isinstance(method, str) and ( + method.startswith("fs/") or method.startswith("terminal/") + ) + + +def restrict_client_capabilities(message: Any) -> Any: + """ + Turn off the client filesystem and terminal capabilities in an + `initialize` request, also inside a batch, so the agent keeps using its + own tools in the sandbox. Other messages are returned unchanged. + """ + if isinstance(message, list): + return [restrict_client_capabilities(item) for item in message] + if not isinstance(message, dict) or message.get("method") != "initialize": + return message + params = message.get("params") + params = params if isinstance(params, dict) else {} + capabilities = params.get("clientCapabilities") + capabilities = capabilities if isinstance(capabilities, dict) else {} + return { + **message, + "params": { + **params, + "clientCapabilities": { + **capabilities, + "fs": {"readTextFile": False, "writeTextFile": False}, + "terminal": False, + }, + }, + } + + +def filter_agent_message(message: Any) -> Tuple[Any, List[JsonRpcMessage]]: + """ + Split agent output into what is forwarded to the client (`None` if + nothing) and the error responses sent straight back to the agent for + client-execution requests. + """ + items = message if isinstance(message, list) else [message] + forward: List[Any] = [] + replies: List[JsonRpcMessage] = [] + + for item in items: + if not isinstance(item, dict) or not is_client_execution_method( + item.get("method") + ): + forward.append(item) + continue + # Notifications get no reply; requests are rejected. + if "id" in item: + replies.append( + { + "jsonrpc": "2.0", + "id": item["id"], + "error": { + "code": -32601, + "message": f"Method not found: {item['method']} is not available, the agent runs inside an E2B sandbox", + }, + } + ) + + if not isinstance(message, list): + return (forward[0] if forward else None), replies + return (forward or None), replies + + +def parse_agent_line(line: str) -> Tuple[Any, List[JsonRpcMessage]]: + """ + Parse one line of agent output into the message to forward to the client + and the replies to send back to the agent. + """ + text = line.strip() + if not text: + return None, [] + try: + message = json.loads(text) + except ValueError: + return None, [PARSE_ERROR_RESPONSE] + return filter_agent_message(message) diff --git a/packages/acp-python/package.json b/packages/acp-python/package.json new file mode 100644 index 0000000000..c179310d12 --- /dev/null +++ b/packages/acp-python/package.json @@ -0,0 +1,17 @@ +{ + "name": "@e2b/acp-python", + "private": true, + "version": "0.0.0", + "dependencies": { + "@e2b/python-sdk": "workspace:*" + }, + "scripts": { + "test": "uv run pytest --verbose", + "postVersion": "uv version --frozen $(pnpm pkg get version --workspaces=false | tr -d \\\")", + "lock": "uv lock", + "postPublish": "uv build && uv publish --trusted-publishing always --check-url https://pypi.org/simple/", + "typecheck": "uv run make typecheck", + "lint": "uv run make lint", + "format": "uv run make format" + } +} diff --git a/packages/acp-python/pyproject.toml b/packages/acp-python/pyproject.toml new file mode 100644 index 0000000000..3d8b716e02 --- /dev/null +++ b/packages/acp-python/pyproject.toml @@ -0,0 +1,41 @@ +[project] +name = "e2b-acp" +version = "0.0.0" +description = "Run Agent Client Protocol (ACP) agents in E2B sandboxes" +authors = [{ name = "e2b", email = "hello@e2b.dev" }] +license = "MIT" +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "e2b>=2.56.0,<3.0.0", + "agent-client-protocol>=0.12.1,<0.13", +] + +[project.urls] +Homepage = "https://e2b.dev/docs/agents/acp?utm_source=pypi&utm_medium=referral&utm_campaign=package_homepage&utm_content=e2b-acp" +Repository = "https://github.com/e2b-dev/e2b/tree/main/packages/acp-python" +"Bug Tracker" = "https://github.com/e2b-dev/e2b/issues" + +[dependency-groups] +dev = [ + "pytest>=9.0.3,<10", + "python-dotenv>=1.0.0,<2", + "pytest-dotenv>=0.5.2,<0.6", + "pytest-asyncio>=1.3.0,<2", + "ruff>=0.11.12,<0.12", + "ty>=0.0.15,<0.0.16", +] + +[build-system] +requires = ["uv_build>=0.10.0,<0.11.0"] +build-backend = "uv_build" + +[tool.uv.build-backend] +module-name = ["e2b_acp"] +module-root = "" + +[tool.uv.sources] +e2b = { workspace = true } + +[tool.uv.workspace] +members = ["../python-sdk"] diff --git a/packages/acp-python/pytest.ini b/packages/acp-python/pytest.ini new file mode 100644 index 0000000000..9c865b9448 --- /dev/null +++ b/packages/acp-python/pytest.ini @@ -0,0 +1,6 @@ +[pytest] +asyncio_mode = auto +asyncio_default_fixture_loop_scope = function +asyncio_default_test_loop_scope = function +addopts = "--import-mode=importlib" +pythonpath = tests diff --git a/packages/acp-python/tests/test_utils.py b/packages/acp-python/tests/test_utils.py new file mode 100644 index 0000000000..dbaa896648 --- /dev/null +++ b/packages/acp-python/tests/test_utils.py @@ -0,0 +1,125 @@ +import pytest + +from e2b import InvalidArgumentException, SandboxException +from e2b_acp.utils import ( + PARSE_ERROR_RESPONSE, + LineSplitter, + filter_agent_message, + parse_agent_line, + restrict_client_capabilities, +) + + +def test_line_splitter_joins_chunks_split_mid_line(): + lines = LineSplitter(1024) + + assert lines.push('{"a":') == [] + assert lines.push('1}\n{"b"') == ['{"a":1}'] + assert lines.push(":2}\n\n") == ['{"b":2}', ""] + assert lines.flush() == "" + + +def test_line_splitter_keeps_trailing_partial_line_for_flush(): + lines = LineSplitter(1024) + + assert lines.push('{"a":1}\n{"b":2}') == ['{"a":1}'] + assert lines.flush() == '{"b":2}' + + +def test_line_splitter_rejects_messages_over_the_limit_in_bytes(): + lines = LineSplitter(8) + + assert lines.push("1234567\n") == ["1234567"] + # Four 3-byte characters are 12 bytes. + with pytest.raises(SandboxException, match="exceeds the limit of 8 bytes"): + lines.push("€€€€") + + +def test_initialize_turns_off_client_filesystem_and_terminal(): + message = { + "jsonrpc": "2.0", + "id": 0, + "method": "initialize", + "params": { + "protocolVersion": 1, + "clientCapabilities": { + "fs": {"readTextFile": True, "writeTextFile": True}, + "terminal": True, + "_meta": {"custom": True}, + }, + }, + } + + restricted = restrict_client_capabilities(message) + + assert restricted["params"] == { + "protocolVersion": 1, + "clientCapabilities": { + "fs": {"readTextFile": False, "writeTextFile": False}, + "terminal": False, + "_meta": {"custom": True}, + }, + } + assert message["params"]["clientCapabilities"]["terminal"] is True + + +def test_other_client_messages_pass_unchanged(): + message = {"jsonrpc": "2.0", "id": 1, "method": "session/new", "params": {}} + + assert restrict_client_capabilities(message) is message + + +def test_agent_requests_for_client_fs_and_terminal_are_rejected(): + forward, replies = filter_agent_message( + {"jsonrpc": "2.0", "id": 7, "method": "fs/write_text_file", "params": {}} + ) + + assert forward is None + assert len(replies) == 1 + assert replies[0]["id"] == 7 + assert replies[0]["error"]["code"] == -32601 + + +def test_agent_notifications_for_client_terminal_are_dropped_silently(): + assert filter_agent_message({"jsonrpc": "2.0", "method": "terminal/release"}) == ( + None, + [], + ) + + +def test_batches_keep_only_forwardable_messages(): + update = {"jsonrpc": "2.0", "method": "session/update", "params": {}} + forward, replies = filter_agent_message( + [update, {"jsonrpc": "2.0", "id": "x", "method": "terminal/create"}] + ) + + assert forward == [update] + assert len(replies) == 1 + + +def test_other_agent_messages_are_forwarded(): + message = {"jsonrpc": "2.0", "id": 3, "method": "session/request_permission"} + + assert filter_agent_message(message) == (message, []) + + +def test_invalid_agent_line_gets_a_parse_error(): + assert parse_agent_line("not json") == (None, [PARSE_ERROR_RESPONSE]) + assert parse_agent_line(" ") == (None, []) + + +def test_initialize_inside_a_batch_is_restricted_too(): + [restricted] = restrict_client_capabilities( + [{"jsonrpc": "2.0", "id": 0, "method": "initialize", "params": {}}] + ) + + assert restricted["params"]["clientCapabilities"] == { + "fs": {"readTextFile": False, "writeTextFile": False}, + "terminal": False, + } + + +@pytest.mark.parametrize("limit", [0, -1, 1.5, float("nan"), float("inf"), True]) +def test_message_limit_must_be_a_positive_integer(limit): + with pytest.raises(InvalidArgumentException, match="positive integer"): + LineSplitter(limit) diff --git a/packages/acp-python/uv.lock b/packages/acp-python/uv.lock new file mode 100644 index 0000000000..25da9e5f42 --- /dev/null +++ b/packages/acp-python/uv.lock @@ -0,0 +1,1335 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version < '3.14'", +] + +[manifest] +members = [ + "e2b", + "e2b-acp", +] + +[[package]] +name = "agent-client-protocol" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/95/2c28ca34e545ce0282e88272aa3a8abdf8f3f926a0492f567ea11acd071b/agent_client_protocol-0.12.1.tar.gz", hash = "sha256:9963ccb590ed96d7dc542c9c023e90676fc76b1c6562e21a9ee112f4fbcb0095", size = 130605, upload-time = "2026-08-16T14:08:22.545Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/21/61/0a2186c8ca53cf464767d4c4fb7fd9e23f48ce4547c2fa0d1c83a4d52410/agent_client_protocol-0.12.1-py3-none-any.whl", hash = "sha256:aaf3cc301ed87d9e2c4ccb91adfe1cd58784355f6ce7e513f50e7ca2391862ef", size = 85929, upload-time = "2026-08-16T14:08:21.307Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + +[[package]] +name = "anyio" +version = "4.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, +] + +[[package]] +name = "argcomplete" +version = "3.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/6f/5a73f04007ca950701765949209f068da628bd11f9c2da287278ce91e0ee/argcomplete-3.7.2.tar.gz", hash = "sha256:aad8b69a0b9969edb62db0d1752354c0d50717b10e0cbb00e2a958381b9fc6b9", size = 74473, upload-time = "2026-08-06T04:53:21.662Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/bd/551ee6af426af84ca33e02622be722925c196608e9127d731ef17c47f06e/argcomplete-3.7.2-py3-none-any.whl", hash = "sha256:6029205678bdd9c1c728a155f5f9ecf5812393f969eef58807641a2bc2aa5b19", size = 43294, upload-time = "2026-08-06T04:53:20.246Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "black" +version = "26.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/57/5f11c92861f9c92eb9dddf515530bc2d06db843e44bdcf1c83c1427824bc/black-26.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:28ef38aee69e4b12fda8dba75e21f9b4f979b490c8ac0baa7cb505369ac9e1ff", size = 1851987, upload-time = "2026-03-12T03:40:06.248Z" }, + { url = "https://files.pythonhosted.org/packages/54/aa/340a1463660bf6831f9e39646bf774086dbd8ca7fc3cded9d59bbdf4ad0a/black-26.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bf162ed91a26f1adba8efda0b573bc6924ec1408a52cc6f82cb73ec2b142c", size = 1689499, upload-time = "2026-03-12T03:40:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/01/b726c93d717d72733da031d2de10b92c9fa4c8d0c67e8a8a372076579279/black-26.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:474c27574d6d7037c1bc875a81d9be0a9a4f9ee95e62800dab3cfaadbf75acd5", size = 1754369, upload-time = "2026-03-12T03:40:09.279Z" }, + { url = "https://files.pythonhosted.org/packages/e3/09/61e91881ca291f150cfc9eb7ba19473c2e59df28859a11a88248b5cbbc4d/black-26.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:5e9d0d86df21f2e1677cc4bd090cd0e446278bcbbe49bf3659c308c3e402843e", size = 1413613, upload-time = "2026-03-12T03:40:10.943Z" }, + { url = "https://files.pythonhosted.org/packages/16/73/544f23891b22e7efe4d8f812371ab85b57f6a01b2fc45e3ba2e52ba985b8/black-26.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9a5e9f45e5d5e1c5b5c29b3bd4265dcc90e8b92cf4534520896ed77f791f4da5", size = 1219719, upload-time = "2026-03-12T03:40:12.597Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f8/da5eae4fc75e78e6dceb60624e1b9662ab00d6b452996046dfa9b8a6025b/black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1", size = 1895920, upload-time = "2026-03-12T03:40:13.921Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9f/04e6f26534da2e1629b2b48255c264cabf5eedc5141d04516d9d68a24111/black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f", size = 1718499, upload-time = "2026-03-12T03:40:15.239Z" }, + { url = "https://files.pythonhosted.org/packages/04/91/a5935b2a63e31b331060c4a9fdb5a6c725840858c599032a6f3aac94055f/black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7", size = 1794994, upload-time = "2026-03-12T03:40:17.124Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0a/86e462cdd311a3c2a8ece708d22aba17d0b2a0d5348ca34b40cdcbea512e/black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983", size = 1420867, upload-time = "2026-03-12T03:40:18.83Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e5/22515a19cb7eaee3440325a6b0d95d2c0e88dd180cb011b12ae488e031d1/black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb", size = 1230124, upload-time = "2026-03-12T03:40:20.425Z" }, + { url = "https://files.pythonhosted.org/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", size = 1895034, upload-time = "2026-03-12T03:40:21.813Z" }, + { url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503, upload-time = "2026-03-12T03:40:23.666Z" }, + { url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557, upload-time = "2026-03-12T03:40:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", size = 1422766, upload-time = "2026-03-12T03:40:27.14Z" }, + { url = "https://files.pythonhosted.org/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", size = 1232140, upload-time = "2026-03-12T03:40:28.882Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/e36e27c9cebc1311b7579210df6f1c86e50f2d7143ae4fcf8a5017dc8809/black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78", size = 1889234, upload-time = "2026-03-12T03:40:30.964Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7b/9871acf393f64a5fa33668c19350ca87177b181f44bb3d0c33b2d534f22c/black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568", size = 1720522, upload-time = "2026-03-12T03:40:32.346Z" }, + { url = "https://files.pythonhosted.org/packages/03/87/e766c7f2e90c07fb7586cc787c9ae6462b1eedab390191f2b7fc7f6170a9/black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f", size = 1787824, upload-time = "2026-03-12T03:40:33.636Z" }, + { url = "https://files.pythonhosted.org/packages/ac/94/2424338fb2d1875e9e83eed4c8e9c67f6905ec25afd826a911aea2b02535/black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c", size = 1445855, upload-time = "2026-03-12T03:40:35.442Z" }, + { url = "https://files.pythonhosted.org/packages/86/43/0c3338bd928afb8ee7471f1a4eec3bdbe2245ccb4a646092a222e8669840/black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1", size = 1258109, upload-time = "2026-03-12T03:40:36.832Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, +] + +[[package]] +name = "bracex" +version = "3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/60/e2a6409041cb3f8ce7ac3d6917b5b30c39f81a0a5b2d414ec30f36fd96bf/bracex-3.1.tar.gz", hash = "sha256:35c69fc105f08d77c02722e8ae707c656ff298dd4d26c89bcb3c98fca8bc3614", size = 29040, upload-time = "2026-10-11T15:26:07.143Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/79/c1ba46e1acd235affd36ce1d3346320ddcda663c9482a9e5f34727db1d11/bracex-3.1-py3-none-any.whl", hash = "sha256:52002b8455658c21f3b6e5eb0f662b57ec9085f42709e58bc86eeab5bc4c37d7", size = 11925, upload-time = "2026-10-11T15:26:05.683Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "click" +version = "8.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "connectrpc" +version = "0.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf-py" }, + { name = "pyqwest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8b/b5/63e14ab9d4d4cc58818562db4120a35fa7454e3035633da41bdc6b712abd/connectrpc-0.11.1.tar.gz", hash = "sha256:18277f7838847b4271ca38d40c7d2387b5a2ea6a29f240689c19e1ec84aaff66", size = 46222, upload-time = "2026-07-15T06:33:31.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/ac/aa647675812cd075f2cb9acb00d62f4f2cbcbc6736049a62342b7371ce5b/connectrpc-0.11.1-py3-none-any.whl", hash = "sha256:8a52e2e92a485fa9681c1101a79a5ebeb31807e3ea3d5aabd41f484a6398bc7b", size = 64991, upload-time = "2026-07-15T06:33:29.396Z" }, +] + +[[package]] +name = "datamodel-code-generator" +version = "0.64.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argcomplete" }, + { name = "black", marker = "sys_platform != 'emscripten'" }, + { name = "genson" }, + { name = "inflect" }, + { name = "isort", marker = "sys_platform != 'emscripten'" }, + { name = "jinja2" }, + { name = "pydantic" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/d2/86c94a2836ed42231653a7ddaefa0a5bc23418167a876bba7376c96b3a35/datamodel_code_generator-0.64.0.tar.gz", hash = "sha256:9c592900a00b20e416494273c22435f5a9aef6ea8c7b9190747522a60497a1cb", size = 1316440, upload-time = "2026-06-14T17:24:50.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/94/71338e2f0146ac10747a5537b3a1e45256e66b7c229869eb0ee787111b41/datamodel_code_generator-0.64.0-py3-none-any.whl", hash = "sha256:b7cd8bd41a312aa997aec6150670bad781847c5b674f17e4d70e78208a0fb990", size = 374698, upload-time = "2026-06-14T17:24:48.809Z" }, +] + +[[package]] +name = "e2b" +version = "2.56.0" +source = { editable = "../python-sdk" } +dependencies = [ + { name = "attrs" }, + { name = "connectrpc" }, + { name = "e2b-dockerfile-utils" }, + { name = "h2" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "protobuf-py" }, + { name = "pyqwest" }, + { name = "python-dateutil" }, + { name = "rich" }, + { name = "typing-extensions" }, + { name = "wcmatch" }, +] + +[package.dev-dependencies] +codegen = [ + { name = "black" }, + { name = "datamodel-code-generator" }, + { name = "e2b-openapi-python-client" }, + { name = "protoc-gen-connectrpc" }, + { name = "protoc-gen-py" }, +] +dev = [ + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-dotenv" }, + { name = "pytest-timeout" }, + { name = "pytest-xdist" }, + { name = "python-dotenv" }, + { name = "ruff" }, + { name = "ty" }, +] + +[package.metadata] +requires-dist = [ + { name = "attrs", specifier = ">=23.2.0" }, + { name = "connectrpc", specifier = ">=0.11.1,<0.12" }, + { name = "e2b-dockerfile-utils", editable = "../dockerfile-utils-python" }, + { name = "h2", specifier = ">=4.4.1,<5" }, + { name = "httpx", specifier = ">=0.27.0,<1.0.0" }, + { name = "packaging", specifier = ">=24.1" }, + { name = "protobuf-py", specifier = ">=0.1.1,<0.2" }, + { name = "pyqwest", specifier = ">=0.12.0,<0.13" }, + { name = "python-dateutil", specifier = ">=2.8.2" }, + { name = "rich", specifier = ">=14.0.0" }, + { name = "typing-extensions", specifier = ">=4.10.0" }, + { name = "wcmatch", specifier = ">=10.1,<12" }, +] + +[package.metadata.requires-dev] +codegen = [ + { name = "black", specifier = "==26.3.1" }, + { name = "datamodel-code-generator", specifier = "==0.64.0" }, + { name = "e2b-openapi-python-client", specifier = "==0.26.2" }, + { name = "protoc-gen-connectrpc", specifier = "==0.11.1" }, + { name = "protoc-gen-py", specifier = "==0.1.1" }, +] +dev = [ + { name = "pytest", specifier = ">=9.0.3,<10" }, + { name = "pytest-asyncio", specifier = ">=1.3.0,<2" }, + { name = "pytest-dotenv", specifier = ">=0.5.2,<0.6" }, + { name = "pytest-timeout", specifier = ">=2.4.0,<3" }, + { name = "pytest-xdist", specifier = ">=3.3.1,<4" }, + { name = "python-dotenv", specifier = ">=1.0.0,<2" }, + { name = "ruff", specifier = ">=0.11.12,<0.12" }, + { name = "ty", specifier = ">=0.0.15,<0.0.16" }, +] + +[[package]] +name = "e2b-acp" +version = "0.0.0" +source = { editable = "." } +dependencies = [ + { name = "agent-client-protocol" }, + { name = "e2b" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-dotenv" }, + { name = "python-dotenv" }, + { name = "ruff" }, + { name = "ty" }, +] + +[package.metadata] +requires-dist = [ + { name = "agent-client-protocol", specifier = ">=0.12.1,<0.13" }, + { name = "e2b", editable = "../python-sdk" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=9.0.3,<10" }, + { name = "pytest-asyncio", specifier = ">=1.3.0,<2" }, + { name = "pytest-dotenv", specifier = ">=0.5.2,<0.6" }, + { name = "python-dotenv", specifier = ">=1.0.0,<2" }, + { name = "ruff", specifier = ">=0.11.12,<0.12" }, + { name = "ty", specifier = ">=0.0.15,<0.0.16" }, +] + +[[package]] +name = "e2b-dockerfile-utils" +version = "0.1.1" +source = { editable = "../dockerfile-utils-python" } + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=9.0.3,<10" }, + { name = "ruff", specifier = ">=0.11.12,<0.12" }, + { name = "ty", specifier = ">=0.0.15,<0.0.16" }, +] + +[[package]] +name = "e2b-openapi-python-client" +version = "0.26.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "pydantic" }, + { name = "python-dateutil" }, + { name = "ruamel-yaml" }, + { name = "ruff" }, + { name = "shellingham" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/68/42fe869ac0f83e060d639b0441980932b2fa68bab85444161111af25d6a9/e2b_openapi_python_client-0.26.2.tar.gz", hash = "sha256:8b71507f0096e94fd836f240a62cfae42a85d393437acf13fb9eef81e23f3891", size = 125758, upload-time = "2025-08-10T15:54:27.321Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/f3/db8b73c07fe3f6a88ba53bdc4b841a6c20e477ec8e7a96ae05ced3245276/e2b_openapi_python_client-0.26.2-py3-none-any.whl", hash = "sha256:15ba028202879184589e9df54b1405a0925ad07514c1f0476eca6c278caed3e7", size = 183327, upload-time = "2025-08-10T15:54:25.538Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "genson" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/53/de162dc8e03fccd9ebe59d17c7812378fe8bd2b604f6b1b94d00165140ac/genson-1.4.0.tar.gz", hash = "sha256:bc7f1c1bae87a21ca44d81149aec95a3f4468d676de9b8b08caa064f3c50b3da", size = 47908, upload-time = "2026-07-06T08:21:50.331Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/02/767f744ab6d4cb7761e5008acc3d534b7a0481af62563d52e391fbcb2140/genson-1.4.0-py3-none-any.whl", hash = "sha256:03bc71bbe52defde70660cc4dcd1ea1097997da5a1cbb90a9dbd3acc7c9e1b65", size = 24484, upload-time = "2026-07-06T08:21:49.046Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "h2" +version = "4.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/85/7c366e69d84c17bb778fe41419e1fbcce3033d5b7ce29bbffff0a98b859f/h2-4.4.1.tar.gz", hash = "sha256:4e866ffb1a869ae14dd9b5e6beb5c24a13da0495ad72b65925ded182521c1516", size = 2157281, upload-time = "2026-08-03T11:45:09.509Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/22/e85faf23bd72a92d1921e37d674ca56eb298a3c8be31fdecef0ff2b3aaac/h2-4.4.1-py3-none-any.whl", hash = "sha256:0e25f1462b23c9cb82d9eb02e28bc706dac2a68cb457c6a0d74d63c8a2a5d0e6", size = 62636, upload-time = "2026-08-03T11:44:59.164Z" }, +] + +[[package]] +name = "hpack" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/5b/fcabf6028144a8723726318b07a32c2f3314acdff6265743cf08a344b18e/hpack-4.2.0.tar.gz", hash = "sha256:0895cfa3b5531fc65fe439c05eb65144f123bf7a394fcaa56aa423548d8e45c0", size = 51300, upload-time = "2026-06-23T18:34:46.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/b4/4a9fcfb2aef6ba44d9073ecd301443aa00b3dac95de5619f2a7de7ec8a91/hpack-4.2.0-py3-none-any.whl", hash = "sha256:858ac0b02280fa582b5080d68db0899c62a80375e0e5413a74970c5e518b6986", size = 34246, upload-time = "2026-06-23T18:34:45.472Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +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]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, +] + +[[package]] +name = "idna" +version = "3.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, +] + +[[package]] +name = "inflect" +version = "7.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, + { name = "typeguard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/c6/943357d44a21fd995723d07ccaddd78023eace03c1846049a2645d4324a3/inflect-7.5.0.tar.gz", hash = "sha256:faf19801c3742ed5a05a8ce388e0d8fe1a07f8d095c82201eb904f5d27ad571f", size = 73751, upload-time = "2024-12-28T17:11:18.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/eb/427ed2b20a38a4ee29f24dbe4ae2dafab198674fe9a85e3d6adf9e5f5f41/inflect-7.5.0-py3-none-any.whl", hash = "sha256:2aea70e5e70c35d8350b8097396ec155ffd68def678c7ff97f51aa69c1d92344", size = 35197, upload-time = "2024-12-28T17:11:15.931Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960", size = 21209, upload-time = "2026-10-06T22:48:38.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" }, +] + +[[package]] +name = "isort" +version = "8.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/7c/ec4ab396d31b3b395e2e999c8f46dec78c5e29209fac49d1f4dace04041d/isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d", size = 769592, upload-time = "2026-02-28T10:08:20.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75", size = 89733, upload-time = "2026-02-28T10:08:19.466Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/9b/e422a865e1d5d57d0e509b4e0bf1c1a70a7f6382c29a5aa428df994c8bc8/markupsafe-3.0.4.tar.gz", hash = "sha256:2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6", size = 153777, upload-time = "2026-10-02T23:07:22.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/55/18dbb4778b30ada5ce071608503cc3edc9e14e13d868c17a6d178fc30f7a/markupsafe-3.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e25feb9e330b63edb0278a0acdf85e50d0cb0fbf49c3084abbe4e24ae195346", size = 11520, upload-time = "2026-10-02T23:04:36.299Z" }, + { url = "https://files.pythonhosted.org/packages/6c/14/0b05f79b4733e264a18d08fe08fa1df7347630ff32a6cb82180d9dccec55/markupsafe-3.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d3391b2188d18737cb2fa147028b1096236eaa7e156446c650a489fa2cadc91", size = 12063, upload-time = "2026-10-02T23:04:37.291Z" }, + { url = "https://files.pythonhosted.org/packages/ca/3a/63ba10b6c1463216b3e4df669a9f0e5a3b0c3071557d2e8229e3968c79fb/markupsafe-3.0.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:849dd2bb0e5e4ab2b71c7191726a4a8d5aa8a610daa584728cbee0b710ddc4ef", size = 24321, upload-time = "2026-10-02T23:04:38.262Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2e/5f015261b76ad633d187ef6f388b413aedd64a8773c4df59e530a0be5525/markupsafe-3.0.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:befb4158af32106b9a93db8d6d1d1cbbd418c0d5aca0cabb7b1780abf0c89169", size = 28845, upload-time = "2026-10-02T23:04:39.295Z" }, + { url = "https://files.pythonhosted.org/packages/33/cf/26e594b26be40c2f1fec63ccf8a8b99d0335a5b2cbe84835c7d82a994375/markupsafe-3.0.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71f88e749ea29f67f21f3b36433c1dc54c7729ed2a6d9e2da2e0d9e0d7b224eb", size = 24701, upload-time = "2026-10-02T23:04:40.348Z" }, + { url = "https://files.pythonhosted.org/packages/81/a5/a513b76c139a3915b43404324e55c0b7979ae4f0d39eb6f075b0282e90a8/markupsafe-3.0.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6da83a088f8ef93b2d483a8232a4dbf4d69d3d8496b568a03c56becac43e1808", size = 22961, upload-time = "2026-10-02T23:04:41.372Z" }, + { url = "https://files.pythonhosted.org/packages/46/cf/4c66192c100b4542bcbe392ae06696b670f66927be3ac38a213234778ff9/markupsafe-3.0.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f0fac8b13d14bb06c68195f849371924ae53dd7b1c00fed24650f704383b692", size = 21920, upload-time = "2026-10-02T23:04:42.429Z" }, + { url = "https://files.pythonhosted.org/packages/cb/17/ac3662678bfbad649893117ada2ba44dc30bf56884e84f13154a792b10f1/markupsafe-3.0.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4a7cdc2a420ca01058182da4253329764d4bfa055564d1eced90e6ba1e8b1d3d", size = 23726, upload-time = "2026-10-02T23:04:43.531Z" }, + { url = "https://files.pythonhosted.org/packages/f7/af/fe47cee339180a69ebca3c57fb3483d0f5cbd1e8337d1871fb1d9c1aebee/markupsafe-3.0.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:83b3944fea42a8400edf92fd1770fb8d0d4f7de651353bd2d8525a92dba69a21", size = 22539, upload-time = "2026-10-02T23:04:44.618Z" }, + { url = "https://files.pythonhosted.org/packages/12/32/d55440ba140442800e02d799c9cb5ab597bf6ebdb1177b5ea39a11f797bd/markupsafe-3.0.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8138eb83940ec7299024d92d4dee45f601b9e6c5ffde9d25f4e35e326203c707", size = 23963, upload-time = "2026-10-02T23:04:45.656Z" }, + { url = "https://files.pythonhosted.org/packages/50/9d/9c86042cb364c2ad4c971e6d1247929effd25f714cd7ee11b05e6316445b/markupsafe-3.0.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:811d02d5122171c1941357efd8f9bf4ffe907b7f0a1a4e729a880e4be3f46e3e", size = 21506, upload-time = "2026-10-02T23:04:46.671Z" }, + { url = "https://files.pythonhosted.org/packages/75/ef/5b824f03ba40c3b3652b6272d083d2fc4fcdd440de519a3a39ba2c3e7262/markupsafe-3.0.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:50b5bedc9ed8a94fc8857a42ef4f84a81ea88f8d4f05dc8705fb23ee6d8dcca7", size = 22955, upload-time = "2026-10-02T23:04:47.698Z" }, + { url = "https://files.pythonhosted.org/packages/1e/e8/44cfcb5ea40e5e43cec7793ef90704ed0c475280839076c4345757eb8e59/markupsafe-3.0.4-cp311-cp311-win32.whl", hash = "sha256:2e5a7cd7fdd14fcb1ae5d7d8bf23d24fbd1daefd1fbca2580132e1ea75f098b5", size = 14094, upload-time = "2026-10-02T23:04:48.788Z" }, + { url = "https://files.pythonhosted.org/packages/91/89/f2b509f7bf79352e40117824c1070dbeafd4df67031d3fa98165a3134228/markupsafe-3.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:fdb4ca07ab75ffadab4a8b135ad59cdbb3156b99310f3d565370da74a15d6bd3", size = 14328, upload-time = "2026-10-02T23:04:49.801Z" }, + { url = "https://files.pythonhosted.org/packages/2b/5a/ccf22672a0f64dc682306e288f0dabcb06c7a201f3bb6e6cbf86d9e8ad03/markupsafe-3.0.4-cp311-cp311-win_arm64.whl", hash = "sha256:569d65055d367e3dcdf30c3f41119467b73d9ee9faf332bdf40402644f5ac08e", size = 14188, upload-time = "2026-10-02T23:04:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/81/09/4c59d56b8461ae8eb0d8ba34bb25b7e618547044679d58a82ef9b2479fc1/markupsafe-3.0.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61631e08084be9e21a8967ec3139c7616ed7c5e9368e05c86d1b39562c8a57b6", size = 11658, upload-time = "2026-10-02T23:04:51.876Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f0/d6613774d86fbf6d145751d43c59875e47a6f9f17daee0aef173bd36d90e/markupsafe-3.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0930db9bdc62d22944e10b066448bb65dc9abe9112880c7cab8da54db4284d5f", size = 12050, upload-time = "2026-10-02T23:04:52.931Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f2/8f18e0b806eb13c1f8d07d917a720831ead54253a6dec011fbc78098a6f8/markupsafe-3.0.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a45c3d514f2436064db00d7fc8778d888f0236ebfed649b53d13a59e69ad51b", size = 24363, upload-time = "2026-10-02T23:04:53.895Z" }, + { url = "https://files.pythonhosted.org/packages/60/ce/fa07dbe8a5675558fa36dea033e19995bc783de2dec5f540ccb9030b06aa/markupsafe-3.0.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1e1451fab512d1bcc3dc26988ec1edb0b82c2db909132872cd9356070a6b63df", size = 28525, upload-time = "2026-10-02T23:04:54.905Z" }, + { url = "https://files.pythonhosted.org/packages/85/40/be87c01f3868ec217f8a2015089d71c22c8c5a75324822e5ed1cdd87210d/markupsafe-3.0.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bd3ce56ae2cbae3ba82b683bc425cd7e48d2ed8b10f3e818186b6f5646d9271c", size = 24733, upload-time = "2026-10-02T23:04:56.229Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a7/aeedb5140afa41fc74c225e9184ab96723a6e873b6ee1c9fede7283456d8/markupsafe-3.0.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e124f974786f831d6043728e38296969d3579db8896fe004682f5758e613581", size = 22985, upload-time = "2026-10-02T23:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/c3/fc/e91352bb08c6a59da3ef0909d457bf95a5f5908fbf151b30a06d9dbcfbb4/markupsafe-3.0.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c02e8f18bdedba082cef725942ac823b9b60656db07f7e265cb31618dfd00d77", size = 22001, upload-time = "2026-10-02T23:04:58.597Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f8/bffee5e7d2a3deb59748a797650a48af7e672025cf641a79344a771ad106/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f098115c247e11d138ab83a28fa0323c77015007ea2df73ba5fd714dfefd67c", size = 23793, upload-time = "2026-10-02T23:04:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/ed/59/b853d6628ecb4d658e1d637224846d5e9bb4adf4f8df97f3be9f29dce2ec/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d5f93ebbeb8032d47e349328ec8662d973d9b05a70b3c35df1f91fe419b84749", size = 22640, upload-time = "2026-10-02T23:05:00.768Z" }, + { url = "https://files.pythonhosted.org/packages/09/b2/1506df394f0f075797c418d0301498f49e43be194e3ffcb49e6fe6ccf022/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:64511c54db4e4987aef4c41923235927428729e8174c5dba488429be70a998ed", size = 24074, upload-time = "2026-10-02T23:05:01.813Z" }, + { url = "https://files.pythonhosted.org/packages/c7/81/5ed69cda630ac69ef60d06c09ba5a7f84ff66a2e28cf986fd5614ab3c6e6/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e1a622f13970d81f95d0c72f9dc090dce9085fccfa4c9f2174377ee32bd15786", size = 21563, upload-time = "2026-10-02T23:05:03.239Z" }, + { url = "https://files.pythonhosted.org/packages/0c/fe/fb1e79be0fea60aa32602ebefc9c35a82bb42b4df157285ab7dfec12341a/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9a7f43c0b202b334cc9184af09bb8f21d3a209e038efaf106936fb69e6b026e", size = 23048, upload-time = "2026-10-02T23:05:04.479Z" }, + { url = "https://files.pythonhosted.org/packages/c8/52/7632a53360671a9b750cdbabaf9cdd89f18b42248b8e4cb42c0b0296e459/markupsafe-3.0.4-cp312-cp312-win32.whl", hash = "sha256:f0ec3b750b59375eab5b0fb2b9254810c00a3375be6d789899f1055a1d556237", size = 14108, upload-time = "2026-10-02T23:05:05.513Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bf/62495e180b7000aaf30000fff849e933f74264638057176cf46852500adc/markupsafe-3.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:11935df9bf455ed0c04eb87bcd720f02b1fe5e02128a9430f23aed6f93336fc7", size = 14303, upload-time = "2026-10-02T23:05:06.538Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8e/4c24208776a65878d656996945aacfbfe010d3720d1a98fc0eb8491fc03b/markupsafe-3.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:a4bbd2d87dd233b9fc5812160c3d0ffbe42edc22a26ce0469f58479ede633fe9", size = 14174, upload-time = "2026-10-02T23:05:07.617Z" }, + { url = "https://files.pythonhosted.org/packages/6d/18/4bc5ba32499e87bb2b0ef5b3a9bb9c00a131fa961ddf0be548cb550f548b/markupsafe-3.0.4-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:de8b364c423ef0a4bad9069657d617f9a5d2b2062457a89b1fa16ee199c399c1", size = 12786, upload-time = "2026-10-02T23:05:08.709Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6f/17f0c099bf25f3e31e63cc19244d9f6af861a9a4ab778c203997903cfdd0/markupsafe-3.0.4-cp313-cp313-android_24_x86_64.whl", hash = "sha256:34bdde374c5932765d7dc685c4a1d191a3207852d67e8e0a9eb6ea85156181f1", size = 12510, upload-time = "2026-10-02T23:05:09.93Z" }, + { url = "https://files.pythonhosted.org/packages/11/af/1a141081b905036ee904ec4bd945e1f70b4e1b32d33c4e59e8cf1d58b247/markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:6bd9e1788e15bfcf6a9082de42e30387e7b85d211ab21e57a939bb8cfaaf8d96", size = 11583, upload-time = "2026-10-02T23:05:10.884Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0a/a89385ae590232622a03e091805cff12f24fabe6c11e0e8bae096cece81c/markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:5066b244f576f91afc8ee3ba029a89f99d39c79b1853fe9d39bea9f0afbec148", size = 11801, upload-time = "2026-10-02T23:05:11.913Z" }, + { url = "https://files.pythonhosted.org/packages/ed/85/ea548dc013962eb73653124bc595635fbf9e0fa41d1f181a967ccb784dfb/markupsafe-3.0.4-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7a83aa6e4805df46fed18e989d3d16f86ef60cb50bbc8d9ce3a6be89165fbf6e", size = 11443, upload-time = "2026-10-02T23:05:12.887Z" }, + { url = "https://files.pythonhosted.org/packages/cc/72/15f2e5ec9cf2eb00d5cdfe968d94e4156a7bd7303832c3f3b2c403a36839/markupsafe-3.0.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2d1b7d9308288661f56672b1b157d75fc536714d3638487bbea17b6318a78248", size = 11665, upload-time = "2026-10-02T23:05:13.829Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e0/4030bea613677e333c8a2c901fd405055f657f9d06acba5b7357984b6ef7/markupsafe-3.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73e77980c7207854f00fc4e71fb1626868d5740ab4012623d55c7a99ad122a72", size = 12061, upload-time = "2026-10-02T23:05:14.807Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a5/28b76a7449eb702966b88bef599e2360b411fbb3afeee8fe560939be06ec/markupsafe-3.0.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7018d4af1cd272e847aa5917983ab5e83e4f6579f9dbfecd4a79c0ca80b144c2", size = 24395, upload-time = "2026-10-02T23:05:15.909Z" }, + { url = "https://files.pythonhosted.org/packages/07/6c/21232811afc3a063b5e934b1ae2efda52f46154ec382f585149c020e61fe/markupsafe-3.0.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c90d5b3d4e944e065a301d741b3c1d784f6bd1f503aa68b4967e32b2ba313d85", size = 27183, upload-time = "2026-10-02T23:05:16.976Z" }, + { url = "https://files.pythonhosted.org/packages/14/38/6ccdfa5b59049cb36fb80cbc80aee9cf1fc9bb77d1335ad435f2070b08cf/markupsafe-3.0.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:18a801868a884f216e784d7d14db2a4077143ce7610440aee2ce8f734e7cfcde", size = 24772, upload-time = "2026-10-02T23:05:18.209Z" }, + { url = "https://files.pythonhosted.org/packages/63/e0/cec6865dfe88cb48fedd4b20aed6af5158e41092adcbf3e028bcc6ec2108/markupsafe-3.0.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:434139499bb20b502ed3baa1f169e618f924a97e7a777fea1a49446d80106cf6", size = 23017, upload-time = "2026-10-02T23:05:19.286Z" }, + { url = "https://files.pythonhosted.org/packages/ee/76/6ed4940bb7648a9aac457c14f870cfdd5105f139a0fb1f29cd61fafa47d1/markupsafe-3.0.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9e227f3dbe6bde7491cf0a9965d00b88c6b1a4a95d11480ddf88bb96d397c19f", size = 22036, upload-time = "2026-10-02T23:05:20.352Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/ed476226d4fe46a09090a36025bf319296810028df55eb12f1253b540f3a/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b8cd1f918b26fd7b1832ece557cc18f2d8747309ff8b3f0ef9d4250c5ad67a39", size = 23786, upload-time = "2026-10-02T23:05:21.576Z" }, + { url = "https://files.pythonhosted.org/packages/9a/35/66ff30450e35ef5fba9ebc930c9411747e537fd9447b65e44f5007e2b84d/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a5fcffb37e602b0b3c1638a97746b9b96125caa9bcf6fa41d337a9261de231ee", size = 22672, upload-time = "2026-10-02T23:05:22.922Z" }, + { url = "https://files.pythonhosted.org/packages/32/0b/72f45ce4b4efcbca4b80cf1b06703eff0be8d37e82abb78f66c85a7ead1e/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5989cb26b2e1efc6a42216a9f6b5ee495ce5ace2e5b352a9af489976b32d1ee2", size = 24107, upload-time = "2026-10-02T23:05:24.175Z" }, + { url = "https://files.pythonhosted.org/packages/d2/03/71776e5fdcba04614b384cc102e8a4198208579d896fd1394cb7cb9aa900/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:add96447a86d205ab616665d53b2950ee81083757f56e6ea833c8b2917646b46", size = 21615, upload-time = "2026-10-02T23:05:25.215Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/801ce02a02e7aee0f784b1ec7843026178f6adeb9c93ac67eb1992a9a84d/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2628d3a8cb648ecebb3c5d6b0a1052d400e4d8b7ac0fb786be8d285b50040d17", size = 23076, upload-time = "2026-10-02T23:05:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/4a/85/c43776625428f3bb4a61e8633940400e3efe6409e3c6f5bff26de5e45618/markupsafe-3.0.4-cp313-cp313-win32.whl", hash = "sha256:672d207103e6b16ca098611b0f9efad6bc00afd47c03d6ef62186495ca677dc0", size = 14118, upload-time = "2026-10-02T23:05:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/6f/36/163da64de88a13db79214ef75fa041be7fa13bdb42261cf5b7484de14bfb/markupsafe-3.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:1f1f9477e174582b0a1b583d60b66e1f2cf5d3fe12cee985e4aedf44766600e5", size = 14313, upload-time = "2026-10-02T23:05:28.749Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a8/9b662783ffaa1149221432a923cee562f78b9cbbb8baa3df9b3753e63e1e/markupsafe-3.0.4-cp313-cp313-win_arm64.whl", hash = "sha256:06de8ef6331f6e822c28d577dc8bf43fe398800477c49498f38fc38b67ff33fc", size = 14189, upload-time = "2026-10-02T23:05:29.917Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c3/a944f3b0df22bd129e96915b9f4e98d2eeca6516687d7618304a966c3c74/markupsafe-3.0.4-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:4ed644d75aa94a2baf7ec3a96eaa160ea58c742eb9d27c6506053c5c40fc84ed", size = 12787, upload-time = "2026-10-02T23:05:30.971Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d6/a44863f69d88b6c7e27889108f70d47aed259edf89d5df3c5fca1eac87d6/markupsafe-3.0.4-cp314-cp314-android_24_x86_64.whl", hash = "sha256:6d2a9efe686f9de00d0d1ea32a4a5a86d558a2277501bd78d964214eab625e59", size = 12525, upload-time = "2026-10-02T23:05:32.263Z" }, + { url = "https://files.pythonhosted.org/packages/17/8f/168ba80e532dd6a93f96f8f706f1ad41d7990b6e1aeedc1cc0d211a33497/markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8781a792a070cf2bd1b86d3aa943894115faaba6e88122a7bf32d62072742453", size = 11589, upload-time = "2026-10-02T23:05:33.251Z" }, + { url = "https://files.pythonhosted.org/packages/32/b3/aa2c95a574d3af39403a469b295886eb9b6d448da568cbebb5a2cbfdc2e5/markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:971a3bbb75d97ae4e2e8f7d4834236f86f85f0c85e04ab2e191db1123b04f80b", size = 11801, upload-time = "2026-10-02T23:05:34.315Z" }, + { url = "https://files.pythonhosted.org/packages/60/d0/34b810107d83840e768bf485de795893ebbae35b26ab061b487adfa0a692/markupsafe-3.0.4-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:8909c2f1c6dd65e054ac4b573a91c8384d1492281e55d82d159d653f7a13adf6", size = 11447, upload-time = "2026-10-02T23:05:35.302Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ab/2f8488f0f817a39fca068d2b17daf446bf5cdb3eae28c3720af534d873b4/markupsafe-3.0.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cf3468d5ec187ffffcaca8e61929a37448f215dafc1386a12c750a72fe53634", size = 11722, upload-time = "2026-10-02T23:05:36.363Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/e2d117b048d47282ade906fbfd92814cbee5647afc13fda88a3406039372/markupsafe-3.0.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:52704c5d36eb6dda8866493decd61111fff86244c9b1ad225ca01b9e91e5970f", size = 12061, upload-time = "2026-10-02T23:05:37.397Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a8/73a81135e85ba66217f5af7facb03bbb386807e1a729ab64532e4c802652/markupsafe-3.0.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1caa2fa5a6184fb233153b35f654e6687bd555476f6170f29d8ee9be1a8b0af9", size = 24434, upload-time = "2026-10-02T23:05:38.407Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ca/fa9216dd01efee2dfdacafe7df32b4d0170fbac694b0c258a193d6e53999/markupsafe-3.0.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:387d8cd30e69b3f0a72877b9ae717033396404e19095b17fe89753a981fda44f", size = 27183, upload-time = "2026-10-02T23:05:39.581Z" }, + { url = "https://files.pythonhosted.org/packages/fa/4e/a469509e538d37af51103b17b073126973f2b1cbf197ff32c7ddf025cfe5/markupsafe-3.0.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:051417f74bcaaefa316276e0ff723f541616ca51043d070da00249d9bddd3e3c", size = 24813, upload-time = "2026-10-02T23:05:40.671Z" }, + { url = "https://files.pythonhosted.org/packages/8f/db/d7282caf7ab03af44d5d6fdbaa019b35c7d7f1c90588b839c07cba640d6a/markupsafe-3.0.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8e9f292fcda89b324f2f5c91d13f1424a153e40fc2756f38ee23b15835ff300", size = 23049, upload-time = "2026-10-02T23:05:41.864Z" }, + { url = "https://files.pythonhosted.org/packages/30/f3/b6a425206e6964efda6acee544d0eb01d1501784d0b8e2dcc74986f33b17/markupsafe-3.0.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:df1ae86ff54725a01fa1a0510b914ca53a161b7050be74f6204e24aded5971d0", size = 22088, upload-time = "2026-10-02T23:05:43.014Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8a/84d3582fc1f0d5bd466cdf2eebf175e172158a6e70701aacec1de1b35430/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8965520ac587c94a4ac48b729be3d8b8de00af39699b17585dfb599babe77977", size = 23846, upload-time = "2026-10-02T23:05:44.098Z" }, + { url = "https://files.pythonhosted.org/packages/1c/65/db101cce51b7ba4864ac491a9859d297dd1adf0e55b103fee9db9c47c527/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:340cbb1957ba99929cbf19a75626d36ba1ae21d1730b287d1cf7f824a20c4fc7", size = 22674, upload-time = "2026-10-02T23:05:45.23Z" }, + { url = "https://files.pythonhosted.org/packages/e0/49/ddee9813d71db0c7a5c9d97c832125e6758a0c844777f1cf076569bb0e22/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3a93d9616ddecfb393727a0041a562cf0b15a244e20f2bd25efc7949be4c4f17", size = 24104, upload-time = "2026-10-02T23:05:46.398Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0e/7d8518d726726870a2399d69fd30d0fa36c5e57a2132c336b58d7c491073/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d2e56fd3b00222722abfb3f5f0759ddbae4b90811b5ad4343c64030ad1bde70c", size = 21637, upload-time = "2026-10-02T23:05:47.48Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b0/b505e8a361ba557dbf3b3aa7331ea39b00d2022a26e925ff8463b9714bb3/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0d9c47709875fdb321452056622e930c52afbc07a7d780762fbb8b4d91ce6fa4", size = 23081, upload-time = "2026-10-02T23:05:48.611Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ea/9cc3cea873f980c75cbdb6f4277ce30ee955de38be0b3d02f14c108e0698/markupsafe-3.0.4-cp314-cp314-win32.whl", hash = "sha256:38fc55594dab834470b6733dead2ee9e3f657fb0608c769dcafa0ba5ab52f45c", size = 14272, upload-time = "2026-10-02T23:05:49.707Z" }, + { url = "https://files.pythonhosted.org/packages/80/f0/5792ff768a410f93ee3f84fc19345295ffc352d2c936b424cb37e514714c/markupsafe-3.0.4-cp314-cp314-win_amd64.whl", hash = "sha256:c1bc67752d5f21013cfe430df4062441714eab79f65a6a05e01505957e9c35fe", size = 14501, upload-time = "2026-10-02T23:05:50.788Z" }, + { url = "https://files.pythonhosted.org/packages/5f/cf/3d074a8edffcc6899355232ff2543ae8d929733239596423b7db79698bc9/markupsafe-3.0.4-cp314-cp314-win_arm64.whl", hash = "sha256:7e1636da3d8dfc220b6dd10264db5f2b165e4888c4518594898fbe381049af8a", size = 14364, upload-time = "2026-10-02T23:05:51.857Z" }, + { url = "https://files.pythonhosted.org/packages/d9/31/87ce42159aae2163cf3bbbd0c44bc87780510eecab1ea3859099aed95dcb/markupsafe-3.0.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:805c8b84534fa10891890f0e4be39f3a99e94615d93e8836bf9fa1fdca2feeb2", size = 11759, upload-time = "2026-10-02T23:05:52.951Z" }, + { url = "https://files.pythonhosted.org/packages/5f/53/b047207eeb7752e960aca3eb1df5fb7eefa7dd4c62ac49bb156456c8a702/markupsafe-3.0.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fa95848c929b6a75f6848d3c9793e59db365ee436776e57db835cdbfa79ba977", size = 12096, upload-time = "2026-10-02T23:05:54.066Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/4326c88a13c7b755657d44b4bb986f8c3d9843ecba7e22d98661d87f9a57/markupsafe-3.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e916035e3e9930cbdfdd10abf48861340221857f45509565898e012263f7b289", size = 25699, upload-time = "2026-10-02T23:05:55.15Z" }, + { url = "https://files.pythonhosted.org/packages/f2/bb/990581b7474bfcf2cf34bed6ba5ea23bd87adb9d671213d68e88620e7a6b/markupsafe-3.0.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b4d12837e0203bbace818ff4a7461afdcd78bcd782351cea148139180d7bcffe", size = 27804, upload-time = "2026-10-02T23:05:56.29Z" }, + { url = "https://files.pythonhosted.org/packages/6b/89/89491878c28e8291f5aa2fffe2c2d57230d10ae366d55dd810b840513d78/markupsafe-3.0.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5086f9975abb1ab531ee6afca1761e4b59a19b446f3f6522ed776963228cfe5a", size = 25912, upload-time = "2026-10-02T23:05:57.416Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/680998b54efdea06fc114565cd739b6d059f826a0279219b218dfa750d29/markupsafe-3.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4a635a0487774f841cb1fb62e907e7195cc95bc761e053184b8acc3ceb20733", size = 23623, upload-time = "2026-10-02T23:05:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/ae/75/2709f5ac5de9467b40b10e2bb8f89cc63dfb74582e09aa734b1124a217de/markupsafe-3.0.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb96e6e088d6cf71c1ea977510948320234824cf226e32f6f6e044f7a9c82b34", size = 23301, upload-time = "2026-10-02T23:05:59.94Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c8/39eadc6c5b14c9c7679bfb98f4d4c6a97863b5beb91839aca4d2d6e16e55/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8b5d563170ff8ba3181caa967c99a3c804d1dedb702c7cb93a6a7c32247da978", size = 24950, upload-time = "2026-10-02T23:06:01.289Z" }, + { url = "https://files.pythonhosted.org/packages/1a/5e/01037f8a43e8ccb0bffb4fbdc5212db05bf080fdd7286cd392332d58128a/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:396ec4e65cc889f69786b3b89478b471cee5a3bcf468b9d9bb03e1a30fb291fc", size = 23330, upload-time = "2026-10-02T23:06:02.441Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f4/23e83ce0596bb0cbe670502d31df8f757bbd01a392aa486fa3b40d1ed399/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:15ba9e28640feef770374b116a6f019c21f52404aeabe516aa7f800587b98cfc", size = 25239, upload-time = "2026-10-02T23:06:03.579Z" }, + { url = "https://files.pythonhosted.org/packages/88/5b/3708897368073cc683d524750474f41a77d2986152c380dcc55b20fdf340/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d920abdfa61279ba1a2ef9484aab07bf03331f8c08a10120fa332353d06e6932", size = 22767, upload-time = "2026-10-02T23:06:04.699Z" }, + { url = "https://files.pythonhosted.org/packages/c6/61/ebda1307864b409e6b3115757a3d4a09cca46cfb6cc65191b5de226b424b/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a9f54054101545a9a9cccefddf54316aa6e4491611fcbef9e91b3b6bebec04f6", size = 23668, upload-time = "2026-10-02T23:06:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/09/15/98075cceac3b5ba0dbb8e4762a847be967d2befc349a2cf2d0ac77f62c9d/markupsafe-3.0.4-cp314-cp314t-win32.whl", hash = "sha256:12a606a492de952afcb43b59a14aaaaad120e708d3663dd0fdf2d738d427a691", size = 14301, upload-time = "2026-10-02T23:06:07.109Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a3/768b560fcc4156685cb563d922b217810cfa7bc135773367f62f1f9d2078/markupsafe-3.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a18f38cafc329bac5e3c2b96c765b4c96d3d103421ed22ab7988c1e3fce27464", size = 14526, upload-time = "2026-10-02T23:06:08.276Z" }, + { url = "https://files.pythonhosted.org/packages/93/63/da554b4c97a6b0ea3229ca7fe8cbfb620be81613d517f482e85958550537/markupsafe-3.0.4-cp314-cp314t-win_arm64.whl", hash = "sha256:eba154571c16e032112afac0dc2dfe9e63c2ceb7aedd07bb7eecf2ce26d4dd4c", size = 14421, upload-time = "2026-10-02T23:06:09.402Z" }, + { url = "https://files.pythonhosted.org/packages/a9/30/54d11c8ca027114898cab97421fb39e4ffd9ddf47cdbc44df2ec76722da9/markupsafe-3.0.4-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:737c9c3981998eba27f11786f84fddcbabc74068b72a4a1f454ea02094b57b65", size = 12791, upload-time = "2026-10-02T23:06:10.485Z" }, + { url = "https://files.pythonhosted.org/packages/10/6d/97c913e253a14bd3cd0e15a5c56d13203b823fa7ee32498342896a072dc4/markupsafe-3.0.4-cp315-cp315-android_24_x86_64.whl", hash = "sha256:489505b03f692c3f376394e49194fa7a7f9e8558d6e293a7056a0032b0c38163", size = 12527, upload-time = "2026-10-02T23:06:11.834Z" }, + { url = "https://files.pythonhosted.org/packages/26/f9/b86d032042a4d597d9e1997f0e5f63a3eedaf11258e0a05760b0a0a826ea/markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:077293e425f28ec737dbcad442a71752e28f8ae27cde3d68acd1fb212091cd92", size = 11602, upload-time = "2026-10-02T23:06:13.122Z" }, + { url = "https://files.pythonhosted.org/packages/f2/dc/73c14c1eedf0ac5fa3292ba43435e6c49d2c2050f33cebde541f8f4807f1/markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9348cbb300d224fe3b89793262cb093504d4ae927004468463f745188a193e4a", size = 11817, upload-time = "2026-10-02T23:06:14.227Z" }, + { url = "https://files.pythonhosted.org/packages/8f/69/2c2fcaa5fcee22d72c7819c0d536fd181c74a688e6143845419579cd2863/markupsafe-3.0.4-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:b807e598953730f82e4eae3bd30f6a122cf6b31c398c6b504c0e04c13c170429", size = 11446, upload-time = "2026-10-02T23:06:15.574Z" }, + { url = "https://files.pythonhosted.org/packages/88/54/9e5ec76c62e6e2834d5a93623018c943e8b3bb41d663e3fd4c03303b9b85/markupsafe-3.0.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:799c39bdf5e2f1292fedd3009f7b3c9e760f10b2420cb9638d56920840ff6db8", size = 11719, upload-time = "2026-10-02T23:06:16.701Z" }, + { url = "https://files.pythonhosted.org/packages/96/24/3ec292b44064c16229e064d770b2625bd8ea941aa61f44905a9fa44942c0/markupsafe-3.0.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:ae9dcb8fbe244cb82f8a6458b455b927a03685e383d9bacf1ea5ce180b96dc97", size = 12075, upload-time = "2026-10-02T23:06:17.855Z" }, + { url = "https://files.pythonhosted.org/packages/aa/85/b64fdb1f304848518742136983c24e96d967bfb59a0ea160e92736901ab0/markupsafe-3.0.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bced6e2a6dba6a28f7dd3c6ce14df1b2dd495923f16ea484cad03decd463b2b", size = 24656, upload-time = "2026-10-02T23:06:18.963Z" }, + { url = "https://files.pythonhosted.org/packages/9c/18/23997d4c65b355da6390d61cd56e0ab3befd6ba8dda25cb40c602bd0fa6b/markupsafe-3.0.4-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3882fb412298575bae3b9c46868251f15cc69307359f87bb1b382e53d6e5a2c9", size = 27737, upload-time = "2026-10-02T23:06:20.117Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/35998dead3c6af88c38265a56e58100211f036234ab88eb2283fd4cbce44/markupsafe-3.0.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04e7902ba80ee4bac1d50a549606527a1dcf0476cd81403db41099d3b60ec653", size = 25003, upload-time = "2026-10-02T23:06:21.284Z" }, + { url = "https://files.pythonhosted.org/packages/82/96/ef49135ce260db4ca4a12b119ed468449cd248db6b1468e2112b546d7a2e/markupsafe-3.0.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:925f929d6b59a8b3f8b8c6ac363cd0af7eecc81efb3071770b3c6717c450a369", size = 23111, upload-time = "2026-10-02T23:06:22.524Z" }, + { url = "https://files.pythonhosted.org/packages/50/7d/83126e338bd88c17a220668235368ad719fd4638e426739858cbb8508f77/markupsafe-3.0.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f68edfc67aabac33708941f26f22a7b8e9f81429bc0cf249fcf7d66b23af8d19", size = 22403, upload-time = "2026-10-02T23:06:23.785Z" }, + { url = "https://files.pythonhosted.org/packages/83/dd/daf7e420de23c8206c365204e7b85e1251d8e19d34196a56336f316e5ed2/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:e5c802729725bd07e2bc3ab7b76dc7e0bbfc53129d8f1eb1c002c24cf774717e", size = 24083, upload-time = "2026-10-02T23:06:25.037Z" }, + { url = "https://files.pythonhosted.org/packages/19/3c/11eecdc06bc44ad5570350085b572ebf049e8f9a38d1ece6d76640b739cd/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:55ffd6ce583d97dc71dc92e930324c8c0d25aea7e3ade6ae54ef77cedb096811", size = 22931, upload-time = "2026-10-02T23:06:26.328Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9e/ac0fd77f2a726e56ecc3ca0235d095feace1358d1b822406c2a2ef26a4dc/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:2cb3dd71fc6be918ad4264346a8ed69485f9b7ed7bf35495d8e22807cd6b8bea", size = 24299, upload-time = "2026-10-02T23:06:27.742Z" }, + { url = "https://files.pythonhosted.org/packages/d7/09/c6bd842ad58ff5b3bc76eeed7e9a42a6f11adc5d090ec697b72c9672731e/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:94f5407f7bc64fa6463906b896f9904beeeb7dd8dc116ee8e9056c8714ff9916", size = 21948, upload-time = "2026-10-02T23:06:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/a3/46/82f586711fed61e86faa1ee1bc317d68cd45a10c8bdbe3f7d1fdf9026ad8/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2dad610540cb2e6272855c178f08ae9a1c7ac258a7fb71660553a5f104b42741", size = 23150, upload-time = "2026-10-02T23:06:30.583Z" }, + { url = "https://files.pythonhosted.org/packages/19/2d/2dfdce99318abbfa26925195fbc17db188c46a1ec6457be121b6f9cfeb42/markupsafe-3.0.4-cp315-cp315-win32.whl", hash = "sha256:03470d1a8268e692ecf79ecd565593e59d44219377a7ead61f1f1b94c1f7ff6b", size = 14269, upload-time = "2026-10-02T23:06:31.949Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ec/6000fd82e8791e58fcd0456ec20f098957e2b03d5ed02eb73241a577c0ba/markupsafe-3.0.4-cp315-cp315-win_amd64.whl", hash = "sha256:d882a373d8093c2941e01291b7ced96e9cbe4781da9a7751ca7e6c70385e5214", size = 14496, upload-time = "2026-10-02T23:06:33.258Z" }, + { url = "https://files.pythonhosted.org/packages/bc/66/e73bd5016421d5d6e2fb6de7dd609f9de020942ac8c626526bd8c6eeaf82/markupsafe-3.0.4-cp315-cp315-win_arm64.whl", hash = "sha256:353bd63081912ab8cfa6a0c7d185934cdf8426f04c618bba6bc4b394f2069b67", size = 14361, upload-time = "2026-10-02T23:06:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/90/df/cb8c3dc98d313a951df2f8968f44e4cb5643df6d3cab749a530ce2f7d972/markupsafe-3.0.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c61750fadcd119d0825bcb7d7d675dd264dcc89cc05292aab5be68ebdbb374ad", size = 11760, upload-time = "2026-10-02T23:06:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bb/4af9b3ca0753d654ac75f9531d5bd741bb77ca6e696f36807c475ffc099a/markupsafe-3.0.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:1c0df495a977d10460a94941799c72d5b5ab03d3858d949b55b5a66c8f371c99", size = 12108, upload-time = "2026-10-02T23:06:37.089Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d4/b56429313aee5fd59b079c3df5615299959e25e7113eb6d8caadbdd7d38a/markupsafe-3.0.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02fa4acbc6a3fc5c693c34d4dd8c1130b7fe99cc915181b0ddd6f72aeb296002", size = 25762, upload-time = "2026-10-02T23:06:38.419Z" }, + { url = "https://files.pythonhosted.org/packages/65/f5/34c181e891aa4f7d59c918584672e0c5eb7fffe76c1387d1246008bf4081/markupsafe-3.0.4-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:05295589e619b9bed252a86b532b8e27350abc372d18ba89b59375325e91ec1e", size = 28400, upload-time = "2026-10-02T23:06:39.819Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b5/ad14694fd0ac9a5ce30bc6498f2999378f418583dd1679cca5a1b512957e/markupsafe-3.0.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:be6cb0c799abb0e2ba3e618e6d28ddddf7e485f6c2ce938dfa237daf3905072c", size = 25946, upload-time = "2026-10-02T23:06:41.381Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a8/26b606445387d0ceb1eb1f21840094b84e4e3c3c3983d80d10b89823b490/markupsafe-3.0.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26e9867520db70d37f7fb421a7f0d8adb40171011fb84ce869afa1a83370dfa8", size = 23634, upload-time = "2026-10-02T23:06:42.748Z" }, + { url = "https://files.pythonhosted.org/packages/39/a2/b8814de672f1f0094d498bf646f2fec9d6356b503d28ef500b71c5095377/markupsafe-3.0.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f03460ff076f70ab595bb45a0205ccea1971443575b6920c52e755dec2b3fbfe", size = 23488, upload-time = "2026-10-02T23:06:44.176Z" }, + { url = "https://files.pythonhosted.org/packages/db/c7/287223376fb73335a3cc5d6eb22c6ab01358cf33945a9c39c06b9dac3f4b/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:436e3ffc6310d3c41878c601db29098102fe5d8a467c49da4a4125254e0980f2", size = 25037, upload-time = "2026-10-02T23:06:45.646Z" }, + { url = "https://files.pythonhosted.org/packages/f9/29/4df8355e313426d19e62ba33e0253c009ca12a0894ee77d67fa67255361c/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:4e2c4809c14559aa7ef426f27fb35afbb38104c349a903bf8f3600456764bb38", size = 23543, upload-time = "2026-10-02T23:06:47.264Z" }, + { url = "https://files.pythonhosted.org/packages/71/e5/8377731e8495668dcc768f645e717df18318c841edaf023a99395f6da9b4/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:da2af0d7aebfc2074080d72efa6ab8317c62481ef1f896f65d9999c1c01f4494", size = 25282, upload-time = "2026-10-02T23:06:48.795Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5f/373456e37ceb1478d657d6fe769cbe0a39f0a8dfc1548eeb19c471eefdd9/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:aa2c838cc024642cc04c6854232f32b43e5e22833dd11119c1766c7873b8370d", size = 22993, upload-time = "2026-10-02T23:06:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/2cbd5628435afb6f541bbaced4bce0c2edac4b09a142e6e928b8b0da9858/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b91cc9d336957239ff200f30097e6fea2dc6d6fb3c81e853eaa09eac904fd894", size = 23712, upload-time = "2026-10-02T23:06:51.759Z" }, + { url = "https://files.pythonhosted.org/packages/81/99/157e10966b033b363aeda5263e82596ee232a0b1d082fdbf90aa417ff083/markupsafe-3.0.4-cp315-cp315t-win32.whl", hash = "sha256:e49fb0d1ce92cfa0cb198cc5b1b11cdf9d0638658e2a2db2687e39db7c87fc78", size = 14300, upload-time = "2026-10-02T23:06:53.241Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/55884815414c9706a23deca150b72c25a62109e65b0b6ce232077802c719/markupsafe-3.0.4-cp315-cp315t-win_amd64.whl", hash = "sha256:4f6e0852a0283b1b1fd776eeb7b766a5f440b3e2bd31ab51af3b400585f3965c", size = 14522, upload-time = "2026-10-02T23:06:54.729Z" }, + { url = "https://files.pythonhosted.org/packages/92/f9/ecbde7149e95b8a0f18e16d5d747f7dc06049d5da2e4f77f6f5e4a1f46a8/markupsafe-3.0.4-cp315-cp315t-win_arm64.whl", hash = "sha256:39dbacefc411633db5b4378b066a9aca70a3d7e2922c9e578d825f844026eeba", size = 14417, upload-time = "2026-10-02T23:06:56.246Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "more-itertools" +version = "11.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/b8/c63995bebbb9ac9c059350e0fb8c82c2420c85aeca7d34e40aecf3df9281/more_itertools-11.2.1.tar.gz", hash = "sha256:cbf08fd0af284dc69718b9b76a8e6203df624d70209ea511e4219d350c856f63", size = 154133, upload-time = "2026-10-14T18:50:30.407Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/57/3c4b1c6f2e0cc29ba023802458ea3d2a996f87085559e4a5f2192d549532/more_itertools-11.2.1-py3-none-any.whl", hash = "sha256:35a7377edd1dd6608dcb2cdf534ded55ea32d49448875ad042cd3f879fb1ded0", size = 74405, upload-time = "2026-10-14T18:50:29.116Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.45.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +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 = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/80/a8/66d45abadff219e36e2a824181b8f6a67e7ed4572934d6252c71c29d5731/platformdirs-4.13.0.tar.gz", hash = "sha256:1aa0b0d3f224c1f07c295121e312a5a24a180d6ae5a8425ea1784b3e3863e9c0", size = 61094, upload-time = "2026-10-11T02:05:24.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/15/1633010b26e88e872c93b67c0b6c5e174fb74cb6fb5c1472b4d51d4a8f22/platformdirs-4.13.0-py3-none-any.whl", hash = "sha256:3dbcf4cd708f21cf876c4eaa90e58412bc4f033d87143f41b1493ff77c25b7e1", size = 32724, upload-time = "2026-10-11T02:05:22.776Z" }, +] + +[[package]] +name = "pluggy" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/db/7fc19e6f2dc92a966727031389fc2e08b558f0f25eb7403c1119ad4713cd/pluggy-1.7.0.tar.gz", hash = "sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8", size = 123304, upload-time = "2026-10-15T09:50:58.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/9e/2b38731e0fc536806f16490e1a12d7f0dc2a1235aa8cc07bcc75416a7daa/pluggy-1.7.0-py3-none-any.whl", hash = "sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec", size = 27082, upload-time = "2026-10-15T09:50:56.808Z" }, +] + +[[package]] +name = "protobuf-py" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf-py-ext", marker = "(platform_machine == 'arm64' and platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_machine == 'aarch64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'x86_64' and platform_python_implementation == 'CPython' and sys_platform == 'linux') or (platform_machine == 'AMD64' and platform_python_implementation == 'CPython' and sys_platform == 'win32') or (platform_machine == 'ARM64' and platform_python_implementation == 'CPython' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/ed/02fd902d9c51b7ff53dfc9a745eb11490722edfd30073af889e171f07b8e/protobuf_py-0.1.1.tar.gz", hash = "sha256:6bd08ac4d8f1661965bbe2685429d79043704cdd1ee720a7a89617331742240b", size = 133525, upload-time = "2026-06-24T19:02:15.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/00/1b3775aca1c70e3007e06ef5996f6bb9b3a32341eb0cce3ffb6effad8dec/protobuf_py-0.1.1-py3-none-any.whl", hash = "sha256:efc4f50f275ed6dae10a1f30bb81ad1a75368557b3ff22a532b7a472050368f1", size = 181656, upload-time = "2026-06-24T19:01:29.556Z" }, +] + +[[package]] +name = "protobuf-py-ext" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/05/6dc9ccff1e8159eb9a144e6d3c4acfd2211cd4fcd20c34fe9155d17d6a7f/protobuf_py_ext-0.1.1.tar.gz", hash = "sha256:e85bfdfdb3ed50634db8ccc7429dd9286520109489c735463971a418707b4fef", size = 31912, upload-time = "2026-06-24T19:02:16.321Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/70/2b5d62a60a2e0d88ecde1ae98db3132bcd2672fb39c8d581b82b34ac0bd8/protobuf_py_ext-0.1.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:310039e03cb15181781a0b78017419f6d4ee302e988c3c70b87f1facdf05532d", size = 306008, upload-time = "2026-06-24T19:01:31.399Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d6/73c06bb4cac2e04c0adc154965fe8b6520224bd737fd7e73bba405056321/protobuf_py_ext-0.1.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89ec8348d1ba045f79b2fedd14e40cca36f2a41b52f2c4fdf55a60c58add2353", size = 314769, upload-time = "2026-06-24T19:01:32.89Z" }, + { url = "https://files.pythonhosted.org/packages/18/c5/e4e6bc6096b66d1c82639a1b501147f16ee65d32567304b987d002e2c666/protobuf_py_ext-0.1.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:182798e4861aba72d05855bd06febe4926aa7265e6f444a1b8af5252beee4f7e", size = 328122, upload-time = "2026-06-24T19:01:34.394Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4b/1d792a40d0f0a0f914f1dfa8bb5e9573ca0ecd5fe5cecf80d77193212abd/protobuf_py_ext-0.1.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9eb25c3a329c0551cc86b209a5e5d8ecb8d834b9924a3aa019377853a703b6d3", size = 492338, upload-time = "2026-06-24T19:01:36.103Z" }, + { url = "https://files.pythonhosted.org/packages/52/51/5ad329223c905e5c530cae38ae24cde3584d8ab7e09457f2a01afce80e60/protobuf_py_ext-0.1.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:aaecbde82bef10c7c40578cbb61b7a19896bf7fa450972050a3bb302acb7d5d6", size = 541578, upload-time = "2026-06-24T19:01:37.481Z" }, + { url = "https://files.pythonhosted.org/packages/ed/c7/01bd8a8bfe2df4b07aafac12ccfb7b7ebe2303f65296a9e02fcafa28ff3e/protobuf_py_ext-0.1.1-cp310-abi3-win_amd64.whl", hash = "sha256:6b0c615c48e95acc53cf33e9310eeaff8b30d2d7555bf93e7bca8fb4f40e9a5c", size = 251319, upload-time = "2026-06-24T19:01:38.946Z" }, + { url = "https://files.pythonhosted.org/packages/24/dc/914065538b5db54b6a920b5af38c1ef142252ea1eea711820435fa259fac/protobuf_py_ext-0.1.1-cp310-abi3-win_arm64.whl", hash = "sha256:72956cd0af5dee24b41c6f5ba5e42622d17e6d555002b5efc1634e27a1446de2", size = 241635, upload-time = "2026-06-24T19:01:40.301Z" }, + { url = "https://files.pythonhosted.org/packages/13/a5/0b5d73cab815fd50615cb87a02e04e8332f9e27380c890aba1459cf7e919/protobuf_py_ext-0.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c6f0cd58620f415a3534d195358338f4999a774e12510f91c592b38d13d37388", size = 304903, upload-time = "2026-06-24T19:01:41.796Z" }, + { url = "https://files.pythonhosted.org/packages/37/a9/14c120b9ac36a0e11bb05e482fa6ae322de583a8e1068e4859035c289947/protobuf_py_ext-0.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21572764f625d829604fc4d83635f533f35775f11c6da142345b3f3c8d64bd09", size = 316148, upload-time = "2026-06-24T19:01:43.247Z" }, + { url = "https://files.pythonhosted.org/packages/35/54/7c00a1a9783c3ba74f6b2f5d2f049f09037bbd489c465c09a34f32fb611b/protobuf_py_ext-0.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7f14f00c2678bfbe7ad46f057b9f9938c1677bcf39e405e163e272c6f9814b8", size = 326458, upload-time = "2026-06-24T19:01:44.655Z" }, + { url = "https://files.pythonhosted.org/packages/0d/0e/81fa50c0d6c4d664e5be6d0a3c4f2c7edfef87ab12d0bac764abca1f2955/protobuf_py_ext-0.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1610865622e2e27568277ca63d8d2d23dcc55eaa767398865c21539ddf4ce24d", size = 493596, upload-time = "2026-06-24T19:01:46.344Z" }, + { url = "https://files.pythonhosted.org/packages/56/19/3183cf6e4de62846c20549654a1d573dae51ca06aaf7fed06accdfab74f6/protobuf_py_ext-0.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8995efba9476e1ea18ef9873306871dba697308994bc2766558fec3387acc3de", size = 539656, upload-time = "2026-06-24T19:01:48.21Z" }, + { url = "https://files.pythonhosted.org/packages/a4/6c/09841f3dbe7c3d4b3f2779f8f2832ac23fb96ac8ab71674e91af732cf9ff/protobuf_py_ext-0.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ba61d49dace8f874361583030a7c48139b42eb37c9ffbb1e7e8a227a51576f44", size = 305017, upload-time = "2026-06-24T19:01:49.84Z" }, + { url = "https://files.pythonhosted.org/packages/52/43/e450b5e202f6715274acc9acd9f9769908cbdeab861a53c798fcbd467d35/protobuf_py_ext-0.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a9c2f026096ca4c595c89a297067ef371e241d3ff8e1f6d7c779aa8419cdca7", size = 316215, upload-time = "2026-06-24T19:01:51.249Z" }, + { url = "https://files.pythonhosted.org/packages/97/5c/23e79b35f1b5755b9bdc0c0c9b8cd8abababaef1a1639608d8a96bb61a9d/protobuf_py_ext-0.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf78707a040b9294e5e1ec4a1875f0046acfe52e92150cea27de4e9fc9db39bb", size = 326419, upload-time = "2026-06-24T19:01:52.93Z" }, + { url = "https://files.pythonhosted.org/packages/e8/de/5493de0ec12920a3b1dd72608ce0c1e8cdb7e0eb9e87accaecc5216df29e/protobuf_py_ext-0.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ae4373845cbb85bdade3ef5368cc2f4b5f80bf173383afc1ab063f95644e5599", size = 493734, upload-time = "2026-06-24T19:01:54.301Z" }, + { url = "https://files.pythonhosted.org/packages/98/89/31da55b414e6332aad11d858e9a86bd36fbb324f52fa3fc6d8f1c57840a9/protobuf_py_ext-0.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2a4cc478eef7a2acc1daaebcde479a3ac2396d47e6bdc7e776ca4c4147ba8b4c", size = 539703, upload-time = "2026-06-24T19:01:55.707Z" }, + { url = "https://files.pythonhosted.org/packages/1c/71/39f231838ef06476d46ac40dd814894277a732a3688c0a0c994850b3b62f/protobuf_py_ext-0.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:359dccdc1c3eafed2a913c570bb082b8df848a1d27d548ce8385f246a7d68be2", size = 302145, upload-time = "2026-06-24T19:01:57.116Z" }, + { url = "https://files.pythonhosted.org/packages/87/f1/01c81ff5f420600366a0e6a613ce2af20834534d2b3d7523f4f3b4ddb54f/protobuf_py_ext-0.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91c38b4a10a306366443273ee03ca554537a0965bf05b7ada8e7dbdee04cc93e", size = 314541, upload-time = "2026-06-24T19:01:58.745Z" }, + { url = "https://files.pythonhosted.org/packages/86/d8/1cbf5a0298ceddc0cdbb28bf1ecaf8bd3cff54ed4ced6a1392d5226172fc/protobuf_py_ext-0.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79eee3bcbb289d6ea114eb8fe3a1469c5b59bf53e207238469f567d9c53ba56f", size = 325092, upload-time = "2026-06-24T19:02:00.382Z" }, + { url = "https://files.pythonhosted.org/packages/af/23/8b1694616044cbfec2d18d4c6fffb03e05089c8bdb5970c6bafd60cc7fd5/protobuf_py_ext-0.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:10992141a8282a71ac3e3530d1a489efb27618d84000b9a47918cf70e5816d9b", size = 491684, upload-time = "2026-06-24T19:02:01.862Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/99997a7a6cf6989c944d9183c5deb6a045c27460add0316c7b34763891b3/protobuf_py_ext-0.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4411ffe0e06a774b83c5c71c546ce097640a25f596c45f95f53d3e3148e3f22d", size = 538048, upload-time = "2026-06-24T19:02:03.362Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/9e99ecbb68e1d5b46016d821eb1cbba9a91e6b50e71e75faf0c1e6189f0d/protobuf_py_ext-0.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55a17d80ea419501ff221a6627523f38a431bb33e6aa3de81ae3a7f271c49c75", size = 296337, upload-time = "2026-06-24T19:02:04.787Z" }, + { url = "https://files.pythonhosted.org/packages/af/f2/305338a28225fb54b28d6c3f5948109b9088b329a2b6f77ca610c2bdcd34/protobuf_py_ext-0.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbb518b5638403ceaa08ac4fc7dac626f45ed9b856b3517de3906cf3de4d632", size = 308961, upload-time = "2026-06-24T19:02:06.185Z" }, + { url = "https://files.pythonhosted.org/packages/60/f9/416103c93677ff2ea407704ea64fa6de9e700dc030c48360a182d91ce373/protobuf_py_ext-0.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a4adc65ab6a5e4885c67fc808bcacb83d755d05b566d312a0e10c2a873f2ad4", size = 321895, upload-time = "2026-06-24T19:02:07.714Z" }, + { url = "https://files.pythonhosted.org/packages/5a/83/eb3e81bb2f83834b7deff4cee2d56eeb1adcbc847492a56b7f910ab257db/protobuf_py_ext-0.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e97f45c49676efacfb8e95bfcb1a002bc337e618a6780be1e55df2ba5ebd2f1e", size = 486091, upload-time = "2026-06-24T19:02:09.243Z" }, + { url = "https://files.pythonhosted.org/packages/99/96/bd88fca38556b3105e4dd41d6a176e31dcc583fe979e830aeedd2f8c20ee/protobuf_py_ext-0.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:53a6b3590f6aa7f97b8ed60f62fef9b096babfeae82f7283fd3a4c405827d4f8", size = 534582, upload-time = "2026-06-24T19:02:11.227Z" }, +] + +[[package]] +name = "protoc-gen-connectrpc" +version = "0.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/24/4a1f13797a3c0c5b179e4f70e8907a303c28376d3ba88ca095e4d58b750f/protoc_gen_connectrpc-0.11.1.tar.gz", hash = "sha256:99a482ca7c35fd755ee6157a4e0eb09171481b06257144dad4503b83b4ac85a3", size = 9418, upload-time = "2026-07-15T06:33:32.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/cf/3c6ba9d5b3153e58338c3768eba3637e531e80349e5553f871c9e2ce2643/protoc_gen_connectrpc-0.11.1-py3-none-any.whl", hash = "sha256:a68441702aab8664d1bef3666173dbd1e7717fc27a04fda65c0a9f24ba27dcde", size = 10524, upload-time = "2026-07-15T06:33:30.559Z" }, +] + +[[package]] +name = "protoc-gen-py" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/d4/ff2a8219fc7caa8991c64a81ed3ae8cb5aabcf9186bce1955c07d9d5b6b3/protoc_gen_py-0.1.1.tar.gz", hash = "sha256:b67101c6b0493e4deb9eaa47731441eabaac29e4d1248a7b8d0e4b5b098539b3", size = 11012, upload-time = "2026-06-24T19:02:18.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/64/1496fa3df28d7100d4c33f9588930cda1dc5c2652031df4f4f433c1558ef/protoc_gen_py-0.1.1-py3-none-any.whl", hash = "sha256:5594044938e1ee8881fce931e9b703fe556f518de6775e4a98d8a50d147a76cc", size = 12551, upload-time = "2026-06-24T19:02:14.077Z" }, +] + +[[package]] +name = "pydantic" +version = "2.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/0b/8e10b2e693af8ec54346a14caf36221334775975a747c9ceaa3f8371d96d/pydantic-2.14.1.tar.gz", hash = "sha256:94f478203dd03404682a1ada216965651dd74b1d2d5ffd62e00e0837caab5c26", size = 900362, upload-time = "2026-10-11T18:37:55.396Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/ea/a56b9fe5066f3537b7882f77e9c5dfb26d8c2eefdaed9b5fc73d57b4dc22/pydantic-2.14.1-py3-none-any.whl", hash = "sha256:9195d967ec791692a04438115466764fb8b9a27b31f14a760437694f40d6b454", size = 488106, upload-time = "2026-10-11T18:37:53.437Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.50.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/24/af4ec4be49fbc810f35b0bdcacc3d433b56bbfa469fd3bfd4ae116cd9bf1/pydantic_core-2.50.1.tar.gz", hash = "sha256:e50d7b94baac6c7d09927fa5ca5800a0c7ee5015c7fcff65beb3a1931b5a6e09", size = 513515, upload-time = "2026-10-11T18:35:44.82Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/1b/ca81811da3d2aea724fb3c4ba6b83f50ab8f210576306ccd601973142d35/pydantic_core-2.50.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c531166c42ea7bdfecc8c50049581f05dd1993b09cc7c52bb36a14e96deaec7d", size = 2152145, upload-time = "2026-10-11T18:31:58.046Z" }, + { url = "https://files.pythonhosted.org/packages/59/20/938ef7e0541af0a314e2f9d63848dbd1656ba2ebfe5ca0469f7ebee00d5e/pydantic_core-2.50.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b6d0c2183008c188e19f4906d426b293bdc4f67ab17df8e180fe16cda208fa71", size = 2045761, upload-time = "2026-10-11T18:31:59.35Z" }, + { url = "https://files.pythonhosted.org/packages/2f/4b/456497fe2affa18f49b226e89bde31e8a204751ee343dd9beaac5438123e/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94be440c03fede26969a5ce75468e0e6a9927a1b46d9b679ee8adc1b057b0350", size = 2049557, upload-time = "2026-10-11T18:32:01.121Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/cd5dfe13928f55e236dff3a1bee06721d318ca6c955d9ca993b0a5d09f39/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:36c426eac0af8d1529ff8467e612b933346caec1fdc0d774f78f67a1a11e16c1", size = 2188153, upload-time = "2026-10-11T18:32:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8d/d0b25bc484f8d9136ea76ece69e0b2d24ab370cc2e7bbd1b79e61723b763/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:028e2f212273d4a39b1ec1e0de8166b1165a65fc0f1111452a9d94fc7c625c63", size = 2388308, upload-time = "2026-10-11T18:32:03.767Z" }, + { url = "https://files.pythonhosted.org/packages/c5/64/6513cac7c9f78492abafa01d57a69de1d246bf3085fd7b0330bf14b025b3/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6f0cc1bb9900dc558960894adeb30b0c083366fc1d69b856209fb2ca5c36fe5", size = 2431187, upload-time = "2026-10-11T18:32:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/ed/60/7f2ae89c37e69a3dc0da9ff57b0388c285542c688d05f36767770429de85/pydantic_core-2.50.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8812592c85d0edf423f10eadcef42716d71e8219085ad9e85b775057b7306133", size = 2157623, upload-time = "2026-10-11T18:32:06.616Z" }, + { url = "https://files.pythonhosted.org/packages/87/02/e3a71373b85a7973099b9a229a4302c7792262717e0b4997e41c01f7dd16/pydantic_core-2.50.1-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:bbce99252ba3167b2b6277f1829d5bf4b43b754524bddf7f944707c3db7d2253", size = 2254539, upload-time = "2026-10-11T18:32:07.968Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d5/5b620aaf40dc631d911565b64d49d3fa8422027c046a7a96dc4e4921ae98/pydantic_core-2.50.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:476f6ed8e43cd1e0b460920e23571700872b284e77331cb30c4faf459cf48a4b", size = 2267344, upload-time = "2026-10-11T18:32:09.399Z" }, + { url = "https://files.pythonhosted.org/packages/e0/48/f46baf4580d8a56ca4782aa65842e16d3cf39736b29fcce8e9c0521b3422/pydantic_core-2.50.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2cbd1b75b09e976ed0d6b6ca297675632ca35df86130088457cdc60ef36970ae", size = 2300877, upload-time = "2026-10-11T18:32:10.903Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9d/e7d28505e6092c1fe075838a4f793ccbe6dd676e44ab16175815ee06cfe0/pydantic_core-2.50.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:5958c72adb417c39b12ac87525ac60b0d73315fcdc59e21f44ee4a5e2512c9ef", size = 2464270, upload-time = "2026-10-11T18:32:12.329Z" }, + { url = "https://files.pythonhosted.org/packages/22/b1/3bf63b82efd93ebc9968f9c12fc46037a28bcb452c88e4688f33e5ff7dca/pydantic_core-2.50.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d8f9e8a6c4ab04b78d61f78627370d834eb004b2869dcb28cfffa647b4ea1980", size = 2478653, upload-time = "2026-10-11T18:32:13.735Z" }, + { url = "https://files.pythonhosted.org/packages/28/17/26d1e4b9c2ba6a92b7dc75bbcb99ef44456b7e3af1bced868403667ea7d3/pydantic_core-2.50.1-cp311-cp311-win32.whl", hash = "sha256:4be846f55c9477f5f3ddde8f2ce941137e16862a56d018ed885d422bb6ae02f2", size = 2120246, upload-time = "2026-10-11T18:32:15.591Z" }, + { url = "https://files.pythonhosted.org/packages/24/65/3e875e7991eaecd4d70686293ff0d8a99aa8f93dc295e6a21c3d702912fb/pydantic_core-2.50.1-cp311-cp311-win_amd64.whl", hash = "sha256:0048b6dddc8ef4b64fccaad878bd143b0c3882ea9936279dc11d613f6b7dd1bc", size = 2176332, upload-time = "2026-10-11T18:32:17.153Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5c/58dbd17c61f209d91c70f549eb7a3bbec7023e89ea12dead091462d6d658/pydantic_core-2.50.1-cp311-cp311-win_arm64.whl", hash = "sha256:6a733778df2f7087ec1100ed0b41533e4f3001976e99570fa34f57c66e7f8e3e", size = 2182471, upload-time = "2026-10-11T18:32:18.49Z" }, + { url = "https://files.pythonhosted.org/packages/ee/94/101b49a6c63f99586755690696eae14532772587d9e39e93f7b8d28a49da/pydantic_core-2.50.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:704075d10b74f2f3c6e15407c696d88701df35fc8953f434a431add0d0074db0", size = 2222428, upload-time = "2026-10-11T18:32:20.228Z" }, + { url = "https://files.pythonhosted.org/packages/db/a3/f8b1b39349c33037c73d7a1b794c9c29d8f07d5bd2b74399c1375a529cc6/pydantic_core-2.50.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e8e1d6ce820aa23317e8209a86bd65a540973c12dc7552b48a4f6c8e9926815e", size = 2025779, upload-time = "2026-10-11T18:32:21.723Z" }, + { url = "https://files.pythonhosted.org/packages/d2/2d/862a7d115305cc621358e51ba13f296e0ffdf4fbd984be9f63bc7862730b/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c18db21573bd2c6489f9a544b7499f0df2853958c568e5e783536ee1f690af41", size = 2048429, upload-time = "2026-10-11T18:32:23.3Z" }, + { url = "https://files.pythonhosted.org/packages/c0/7d/fed95d18bf2b5abe2d5ab2bbec4184bfc22b6805994a33ccdae8edffd25a/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb57f304525a5e3c13333b772bf9a473f36326e9c821b2e8e1b2fd36f80ae2c3", size = 2173274, upload-time = "2026-10-11T18:32:24.697Z" }, + { url = "https://files.pythonhosted.org/packages/93/1e/c6f22ceac65dc3d291ff79bea0eb74cbd1de8b128f01fee48af03bcea1e1/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a27c09d86600f1bf2fe3f37e1ae697faf3143931c09322cd799da94deee923b5", size = 2360403, upload-time = "2026-10-11T18:32:26.281Z" }, + { url = "https://files.pythonhosted.org/packages/2c/97/190b5b6bfe4c72f265feb657913eb30749193927a5ad7b83cfa7866a25a2/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46b3301d3b5c886f77de7546e47274a5842c622ea2020b8c6524c6b66913b4a6", size = 2439799, upload-time = "2026-10-11T18:32:27.844Z" }, + { url = "https://files.pythonhosted.org/packages/90/24/48cd98388e4ac2c08d0800af36d15413db15e20387f1038123a2c86cb2f6/pydantic_core-2.50.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ba4e9d8210d941c200431a56b2c0400b131865947903937ed3ec5404307d2e", size = 2160157, upload-time = "2026-10-11T18:32:29.553Z" }, + { url = "https://files.pythonhosted.org/packages/00/86/d909133d0b9979ed14c20f110f21124a0e346caf071bd66ac5af3009062b/pydantic_core-2.50.1-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:e5faeaee74a57d32b3ab3aebad2e348f06d3ba946fc5d28c1728455f00a3d13a", size = 2229523, upload-time = "2026-10-11T18:32:31.027Z" }, + { url = "https://files.pythonhosted.org/packages/90/31/4b43f08131b2098e73a852fd932063385f3103b33839fd9d1abdb89e673d/pydantic_core-2.50.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3cda0e538208e5d722bbf3698b24f19c0a7d05bc8d5f8a7f9b121ea7fa243d9", size = 2242700, upload-time = "2026-10-11T18:32:32.622Z" }, + { url = "https://files.pythonhosted.org/packages/88/77/de39658569dc7a4c293d110a89629f61145e7538f00d1e4280c28debefd3/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:57f51b31ff826e2859120cf4737c5a758a48d96f3e97da40ccee1796d58078ff", size = 2278452, upload-time = "2026-10-11T18:32:34.165Z" }, + { url = "https://files.pythonhosted.org/packages/46/bd/1eeb9e4e791e81210ef9096f58080b32ba15b5b13c108d50aaa3f8ce9f73/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8daa7ee75245d43ad7d747e5c9ecc1b1d06552f72b14887e9276f787d57375f4", size = 2450098, upload-time = "2026-10-11T18:32:35.547Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e6/b2accfdff17e5a88d6358536702146de00f9339adaa4d5bbe1ee93cb4832/pydantic_core-2.50.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:acbf31f37c53a5ac0c34706c80b4f5107ba20b05fdd3816124bf236ef0c57dd2", size = 2476839, upload-time = "2026-10-11T18:32:37.119Z" }, + { url = "https://files.pythonhosted.org/packages/3b/7e/c8a83eb5d2cd42c2e13507150fb777b1edea5a018adc9cbf2858a1d08285/pydantic_core-2.50.1-cp312-cp312-win32.whl", hash = "sha256:45b11cac094aa25725581d9304eee93c9028516b9ea80dd9e175e13a5a2c840e", size = 2101425, upload-time = "2026-10-11T18:32:38.593Z" }, + { url = "https://files.pythonhosted.org/packages/c5/17/8bbd530b8659e9d963f5b16f6cb8e159f4cd174b4d6304e48969ab12dba8/pydantic_core-2.50.1-cp312-cp312-win_amd64.whl", hash = "sha256:132529c83901437ff642f585216831bf5fd7a91df66829907e155192ead62498", size = 2172522, upload-time = "2026-10-11T18:32:40.112Z" }, + { url = "https://files.pythonhosted.org/packages/af/9f/a79d690b127e4e3cff143d031538ab9aa715f6b2fbeecbd07cd6f2bf9fb7/pydantic_core-2.50.1-cp312-cp312-win_arm64.whl", hash = "sha256:4e834f6a8e4ff772dcc34f58ef5504147a3ea5b0f4eeb13b0f8eb2ca75ac57f1", size = 2161366, upload-time = "2026-10-11T18:32:41.606Z" }, + { url = "https://files.pythonhosted.org/packages/ae/57/0e237d7091d2cd44a35d243b7344227f90440166860469cd036afc23a04d/pydantic_core-2.50.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:d5e062c01286d861fd6a1c4ff6e063547b3e713067f2df033c0ff97ac2ca006b", size = 2222774, upload-time = "2026-10-11T18:32:43.103Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b9/c720e56858d4e1539503297ed37063e0c08e0f3541c41b777f6a800f75fa/pydantic_core-2.50.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c003c3b7f49debb893d2d85ae099ac5959c9839e2f330fadb1fcdf7a6594482", size = 2024738, upload-time = "2026-10-11T18:32:44.587Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b8/fbfc25875219cc060e613170ff10e850c6d8924beb4910da57c2ee3ba1d2/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:409e0ea40ec30d9158f33574fd758e689f6045a0f2596701828c27816ca9687d", size = 2047049, upload-time = "2026-10-11T18:32:46.164Z" }, + { url = "https://files.pythonhosted.org/packages/d7/43/34210124d504c553688f2f04b48500b131237528ac545b445e0d6d30e0d5/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:131059670f1d2444269b8585cb888963994871932447c08b39ac6a51fcfef658", size = 2173415, upload-time = "2026-10-11T18:32:47.722Z" }, + { url = "https://files.pythonhosted.org/packages/65/cf/6e178e8fdc11da5965bef980983bf46326a42f436871dd51ba0a57f39df1/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dbcbee53bf17196a7f745aa9bf5a9603953a1e365b1f020be3207c676a3e7c4", size = 2361719, upload-time = "2026-10-11T18:32:49.217Z" }, + { url = "https://files.pythonhosted.org/packages/11/14/bd5169d356aa91bf777e28ba0b281c192d286d03c2812c9c9db023a2f00e/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:325c23f3e35cfbf0fe3486fa5f7260d1e45885173002d30a28ca019994124255", size = 2439803, upload-time = "2026-10-11T18:32:51.025Z" }, + { url = "https://files.pythonhosted.org/packages/1c/bc/d79d000e5203ebef39af839f2ce77a777fcad6e08ad26af9a2fcd114ffc8/pydantic_core-2.50.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17e722e156d0444ecaefbe640bdb60928752bf2013e2b7a11cdb099aaae19bec", size = 2158402, upload-time = "2026-10-11T18:32:52.714Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a5/4902cb5fd599422c130bd3124ab31ee8b771199bd56662702eed07d3fec7/pydantic_core-2.50.1-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:aa8224f10880d9bf1b5993988ba153d42a8b4f3f4f511f93b1f09c93ff613c72", size = 2229755, upload-time = "2026-10-11T18:32:54.126Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f5/c1481f8669f6060d89110c9b1374173fc8767ca276b84f4870bd8acd5e3f/pydantic_core-2.50.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:41bc8237121bd8dc8d888dfd6279fc166ffc88c1f1bf3a8bf00869680533ca4c", size = 2242303, upload-time = "2026-10-11T18:32:55.641Z" }, + { url = "https://files.pythonhosted.org/packages/04/f9/77fc3c7653ba9b6e42049e17e96b25388187d4a7c274ab1cb07f58ac8419/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:45c6266d071c241f2a168d45bf8c54344f0effce35e7e6b73afdec11f3687568", size = 2278345, upload-time = "2026-10-11T18:32:57.38Z" }, + { url = "https://files.pythonhosted.org/packages/95/9b/0579c5d12e7f2b16b27e6782427987341fcce07b0725e02ddb0b74add0c4/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:1deeacb112d14d3f4fcb16b165f7dbaf76c70ba6e82f37ba042bdab51970a0b8", size = 2449947, upload-time = "2026-10-11T18:32:58.896Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ac/1b677db91eba54cc5922f4de6edc46ba45c2bd712dfdc0130382d158e214/pydantic_core-2.50.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1c96fd793b73d1b92e65570132505498fe7b21eaef73cdf74e67e5dfba7ac9e4", size = 2476889, upload-time = "2026-10-11T18:33:00.665Z" }, + { url = "https://files.pythonhosted.org/packages/4d/2a/3a9f6624ee3ea9ccba5249dde11418bb4c35780a7a92608f8768bd3fea39/pydantic_core-2.50.1-cp313-cp313-win32.whl", hash = "sha256:06ead20d39ffd6f2f6f2a8f8a6de67ff8bb1b4f14a8a30e058502514ee2ac685", size = 2101118, upload-time = "2026-10-11T18:33:02.329Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1f/323f78ddd9d9938aac420c1abb4e8ba799fc8bdab0acc67c0593b837c979/pydantic_core-2.50.1-cp313-cp313-win_amd64.whl", hash = "sha256:7816e98acc08119dc0f340ab167048ecc54126316330c1f0caf7c6756c88e28f", size = 2170840, upload-time = "2026-10-11T18:33:03.919Z" }, + { url = "https://files.pythonhosted.org/packages/cb/09/8497c52a739ae425c3ac2f7f56414cbc711c67d374346174f40fe2062644/pydantic_core-2.50.1-cp313-cp313-win_arm64.whl", hash = "sha256:c17799a62c142d61b8a3c51752a7cbc87fe2ad4ccfab10e628a77b405075c662", size = 2161633, upload-time = "2026-10-11T18:33:05.518Z" }, + { url = "https://files.pythonhosted.org/packages/49/33/28b96e81677153715e3eafb9f26663a80841e859fde282a380359b0d3fa1/pydantic_core-2.50.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:1cf41f1ae3fa155cf167a72689ad044bcc1e3c97e064123677149bdfb5dafc4a", size = 2224428, upload-time = "2026-10-11T18:33:07.153Z" }, + { url = "https://files.pythonhosted.org/packages/94/40/15c06410c9b7b8da5805d27b64e09bd3f900e986728106db2689dbc51513/pydantic_core-2.50.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4df197990c15b5a37c5a277d131d9f2c67de6133f2e5dafd80d9bba4b99f46f9", size = 2023740, upload-time = "2026-10-11T18:33:08.763Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4e/5eb629f6efc2a27e421d789dd4bfacbb5f6d09c80c28b13d1e87d73163d5/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0036473f5583e6a60e50b8b21651511564277a3f05cc5dab8cf579f552cd5f6c", size = 2049435, upload-time = "2026-10-11T18:33:10.366Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8e/f195aebec49ad12318876ac2368c197a7939f5d52f8e156d2238ef8a4588/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:992c3514ec891fa7858099183e4d64e6bd5a5d4ff452fae29df22faa77a006bb", size = 2172975, upload-time = "2026-10-11T18:33:12.368Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f5/7ad9fb83010cd5ea0948409db105676ed779c4e709e2d3d89b9fe2558794/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:739dc730e6be3bd5ec2f4ab5cfc7eb047cc45fc1497b3bafec74ff2ed07df597", size = 2364197, upload-time = "2026-10-11T18:33:14.244Z" }, + { url = "https://files.pythonhosted.org/packages/ca/fb/bf0aab3e78301d202b82a0322ec968cd703b11fc0623fde9a28708936f62/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32fad3a91e51b6d2039c572db04a5a873260b399f6bd62c3552671fa7a4a2899", size = 2444259, upload-time = "2026-10-11T18:33:15.79Z" }, + { url = "https://files.pythonhosted.org/packages/45/35/38f6d6564fae57d9b12e5676dfa947e0e7d5c46dbeaa7eb3352261d6d299/pydantic_core-2.50.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42b54c2c90ad348b5e3a85e03e715d572c1fde357ef104cdfe3b03b697a404ea", size = 2159857, upload-time = "2026-10-11T18:33:17.51Z" }, + { url = "https://files.pythonhosted.org/packages/65/a0/fb0a3ca10f139dcf765b2d312d10cf0f65c57c59993229d00ad12b14ceff/pydantic_core-2.50.1-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:2df1ff41884de2bc4b307bafd7c40a691094fad2ff8e767e5b45a319257bcf4e", size = 2233954, upload-time = "2026-10-11T18:33:19.591Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6a/e63842252702aa4ec6e6b7178ca85e541a77459076592c23ebe2d9840b33/pydantic_core-2.50.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe90228920fd8ff2be62622b6bb8a2b11acd65046d50c6b130614b5879605a20", size = 2243876, upload-time = "2026-10-11T18:33:21.393Z" }, + { url = "https://files.pythonhosted.org/packages/39/25/5991cf8318b37e0dfab47b87541619a1df8501a793cba0d978846cba37a7/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:844b869f118e22a41a091bdcedda8a71bc1b0f62c38d1a0c3211cece47e1d8fc", size = 2277908, upload-time = "2026-10-11T18:33:23.324Z" }, + { url = "https://files.pythonhosted.org/packages/a7/3e/3ee8baaa6cc25a6961c69168cf9ff0f002d56f4e0d541ad6724c18fb61f3/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:2eb75304506894a281d346220a4f7481a1b8729577c5ed2a05395991966a8396", size = 2449285, upload-time = "2026-10-11T18:33:24.942Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/acbec6deac13fe697a80c275a9b6661db62c4d323bac8a47347b1f39c7cd/pydantic_core-2.50.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6b20a4bffabdad0db2927ac034ae3b8a681b1f7a0182f3e60b479ad2fde21ebb", size = 2479013, upload-time = "2026-10-11T18:33:26.925Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/21a3f237b264389f6219d053ee78fc1b5c2fd402c1b1cb2b6cb8b85f9834/pydantic_core-2.50.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:99ba9bc2b8062ea0c326a990f7f00e6530c23579de66dd246e72c4cafef950a5", size = 1484400, upload-time = "2026-10-11T18:33:28.693Z" }, + { url = "https://files.pythonhosted.org/packages/09/ce/077a6d262d12ef09108377ac0717f029f420d773ace63cafd6143af75568/pydantic_core-2.50.1-cp314-cp314-win32.whl", hash = "sha256:cf356f70551d40374eaffb1aa63f1eb6d2006681cbd7a9faea173ce0f4dd7cd2", size = 2103222, upload-time = "2026-10-11T18:33:30.326Z" }, + { url = "https://files.pythonhosted.org/packages/14/4c/350a2415209c43d670eb71d3c040f332c04a30583d39e311b05c7ac15762/pydantic_core-2.50.1-cp314-cp314-win_amd64.whl", hash = "sha256:d32f3acc081cc3923386d88f422cde8892335e95f034e0104bb4cf9310d9915f", size = 2171586, upload-time = "2026-10-11T18:33:32.139Z" }, + { url = "https://files.pythonhosted.org/packages/bf/92/9bea6ca96580a0902fed366f064f0829e404f41889b34543279a1162b888/pydantic_core-2.50.1-cp314-cp314-win_arm64.whl", hash = "sha256:bed5163e03b98bc1fa2eb05d74c63d9c5c95d8ed6254985481640fbf5e237dea", size = 2155773, upload-time = "2026-10-11T18:33:33.896Z" }, + { url = "https://files.pythonhosted.org/packages/86/8c/f121f073cf32bdd5cba7e6230b1ce0ac845b0cf43e44dd597d95af272db2/pydantic_core-2.50.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:9572c1369e9c9da2d64a7b7992c786d90ff295abc93964cfe3125e4290768070", size = 2234017, upload-time = "2026-10-11T18:33:35.588Z" }, + { url = "https://files.pythonhosted.org/packages/a4/69/2bb2bcd6146cffe98d760a46c42ae71efd5151d9b2f9c9bf6619a3b32083/pydantic_core-2.50.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2005207aafe1231315718bf6ed5d064a7300fb4772754af35ee72fc68159492e", size = 2001016, upload-time = "2026-10-11T18:33:37.57Z" }, + { url = "https://files.pythonhosted.org/packages/20/b3/fbf854c7d07ec114260c26e9e2071a4381740f9ae09641dbfcbdf2a18c45/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f6047f62a6c5ae08d0a6afb035667aa2d97c3d20d69762e034c5ea144d92a5", size = 2023015, upload-time = "2026-10-11T18:33:39.433Z" }, + { url = "https://files.pythonhosted.org/packages/65/20/6de55b2f92cdb614b745c6e9ced639fc4fd7e1e77604825a88bdece6fcbd/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ef800dd7d85bcdadf4c3076e4c94e43939493558a3b69a1ea830c706d4617bb", size = 2192543, upload-time = "2026-10-11T18:33:41.381Z" }, + { url = "https://files.pythonhosted.org/packages/0c/d9/19e91c94bd5c405945ce2f15526808aea37e162c253160de8ed7bf70b406/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b0135bcdcaa0f23573f286e4cb5e0fd2962700964ed13df085b85f2b97aeab9e", size = 2388704, upload-time = "2026-10-11T18:33:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/b6/bc/2e24c8415eae1a25ee5a5484946a917123bad2e9d01689a759236928175a/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0b3a6f334c6a2345ca15318ff894502a90012536404b37c844a976c76c846e0b", size = 2464118, upload-time = "2026-10-11T18:33:44.856Z" }, + { url = "https://files.pythonhosted.org/packages/bd/1f/945b8053cb061c64e102bcaf7bfb9ed740c0bd4349f9bb978a7d4ddff4ab/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06e01fbbfdb9be777b316a71b6c49efaf4a08b615d0a98d678cda3023f79d019", size = 2150142, upload-time = "2026-10-11T18:33:46.661Z" }, + { url = "https://files.pythonhosted.org/packages/2b/78/96a3e50bf0d64aaae781107eb9335b7529d05a85793ed6e7241c4cd1d931/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:a29a061fec0b4e2d714f277e70a3a18125ecff803f2fea6eade2f2e53711d112", size = 2245577, upload-time = "2026-10-11T18:33:48.574Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5d/a6038a0322232758a6ebfa709f4ca14a60bf5b93940cd0b345056a557da4/pydantic_core-2.50.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f5187624823423e1d1b82b1072ac41dc837389e18d3d0572cc19bbee46cd550a", size = 2263728, upload-time = "2026-10-11T18:33:50.527Z" }, + { url = "https://files.pythonhosted.org/packages/0d/4b/76ded3333a457a9344c4f2d63f2d82d0101654b05178fa4ddfe7d3627674/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3e46a9eb0a0901dd6275e6b06ac3a464885ef350ec4121fe486869de8053e4bb", size = 2293630, upload-time = "2026-10-11T18:33:52.362Z" }, + { url = "https://files.pythonhosted.org/packages/a9/00/9eca378335c9c1b72bc779bf6e4c2a82ce784f14ec48a0e87102c9870e05/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:756d669f04e62ec4148ecfe22be6a4484d9b1181a6ef32e205ebfd200540858b", size = 2468048, upload-time = "2026-10-11T18:33:54.118Z" }, + { url = "https://files.pythonhosted.org/packages/35/ca/e3832e9cf93651251de43c8c5c029ed680a3c1a0a1b17ee26c81bed08cbd/pydantic_core-2.50.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:c516cc5367ca3448995d42cb994bf3f4c9002d2a7c22eac9622551269ad1b807", size = 2512159, upload-time = "2026-10-11T18:33:55.99Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f2/773469b5a10a39116a2cb17edaf6d722f017dd8da07161b371465311c542/pydantic_core-2.50.1-cp314-cp314t-win32.whl", hash = "sha256:9d1bed94af6a63835461f3cf7502058eb166c58c4778e11d0f433cfb1bd69e19", size = 2098406, upload-time = "2026-10-11T18:33:58.043Z" }, + { url = "https://files.pythonhosted.org/packages/ee/42/0bb74f8f25204b259b11ab7c12dc7f180893b6bd706118b385147fb6efd5/pydantic_core-2.50.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c8dce1f1e0e5358b682a6ad3fa5e31b31d4560997b8e61417e9217c8d60f8a0c", size = 2148481, upload-time = "2026-10-11T18:33:59.913Z" }, + { url = "https://files.pythonhosted.org/packages/47/0d/d801646c9679a4e630e15cf521d4108b93854b42a6e6e02391bd4c6b1095/pydantic_core-2.50.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ceff0acc940be2715bd6ad17b24c0e5304abf44f6efd0f81ee8499e640f9dc86", size = 2174972, upload-time = "2026-10-11T18:34:01.875Z" }, + { url = "https://files.pythonhosted.org/packages/b2/84/23984b763d8862a02a13d27a44b6e8169428fd85ecfed88f54c29108604d/pydantic_core-2.50.1-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:8a6791afa2245e6c6b180122d105941644f5bd410bb18623b408808cc41a3102", size = 2224599, upload-time = "2026-10-11T18:34:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/4d/90/a63cf8586abc1d1a3f6d9b18f0224789ebf003851f092ebda3c7c863fd32/pydantic_core-2.50.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:84f34323a61a365b4e9295de6028474754829aaddd59c7bf1a040e7487ef8f3c", size = 2024191, upload-time = "2026-10-11T18:34:05.901Z" }, + { url = "https://files.pythonhosted.org/packages/a6/6a/f34bff9808ffb4907fbf5f5040457d253cacf2da7fce9efbc99ca1b1a44d/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23edad659e8dbd8ca7e4e877fe6c81573abbdf215bd25a68b53e1272f58b80c7", size = 2048252, upload-time = "2026-10-11T18:34:07.757Z" }, + { url = "https://files.pythonhosted.org/packages/b2/54/13f419bf1eb59852003818e25935aaf175687d978f4c4bca70e08fe40a3b/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a5fce22f1e87d181e924e12da7d81cfe031fb3881a5ddf26ad28f141756ca43", size = 2173376, upload-time = "2026-10-11T18:34:09.592Z" }, + { url = "https://files.pythonhosted.org/packages/6d/6c/b5a34d24cd0c81669d8f8339d74e6815abcf2f8fb48ab4b49b84c09be1d5/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c73622ef819328873b53109ee4f77ceb598bffedd02daf916102be3228866b78", size = 2364559, upload-time = "2026-10-11T18:34:11.534Z" }, + { url = "https://files.pythonhosted.org/packages/eb/8d/d64d6216a8df365082665927ff923f183056f9049fee08e9777c9ac05296/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce8c25ca38cc0e3d7753ba180808de2c0c8cb24eae0df64491e40921454e9831", size = 2444697, upload-time = "2026-10-11T18:34:13.535Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0d/1b1149f60a00ea21ba5f70e28acbd40feb4598af414f80f53c921fac07c9/pydantic_core-2.50.1-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7689580e72a642ab5ec64d5f55b2e33636fa43b4ebe63c0c2c965ef307c7d1aa", size = 2159961, upload-time = "2026-10-11T18:34:15.56Z" }, + { url = "https://files.pythonhosted.org/packages/1e/35/f236549299dcc78e71e945495d7ec67e78d20844d0999c60601685003031/pydantic_core-2.50.1-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:d5c0e32fdbce7f1e8ef4d11f655694bf5f4175c757a9f1dc2be09b8864e5bcf5", size = 2234581, upload-time = "2026-10-11T18:34:17.502Z" }, + { url = "https://files.pythonhosted.org/packages/6a/85/26901a490522b7f75ef9bb9a7afb73e5bb550f0e1cb5b99a0069183e2eb9/pydantic_core-2.50.1-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40f523349960fa30f3ea51404308ff50f9997a90df639590f47a057c1f32b415", size = 2244090, upload-time = "2026-10-11T18:34:19.402Z" }, + { url = "https://files.pythonhosted.org/packages/ca/2c/481bcfc70ceeb77a778ca6e5b705fe592cb64735c108157f81b7dd9c280e/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:d4193206b6587047437f6f11d7e776df23e1c1e23af2a54d9347275614791e10", size = 2277948, upload-time = "2026-10-11T18:34:21.317Z" }, + { url = "https://files.pythonhosted.org/packages/24/eb/f1e09333faa7ba447cde967310f758430cc7c5e987bdab5228816c70030d/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_armv7l.whl", hash = "sha256:84bc765b282a9d5b7fe0348b8648904f25a6a04b2139da52b1dd30c8ac3a2c8f", size = 2449688, upload-time = "2026-10-11T18:34:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b3/036bde636db8f76d92996e81aefc75678ab5cec4a07eea1ad0c72a893fc3/pydantic_core-2.50.1-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:ed1e728b39a383c81035b2459cfcb35d99dfb01f7d6ebe3a913bc1cc5b81e459", size = 2479286, upload-time = "2026-10-11T18:34:25.214Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a3/07f018294ee18d144afeb6df1a47d9e92960f014be45c5ed13519fa5af95/pydantic_core-2.50.1-cp315-cp315-win32.whl", hash = "sha256:bc94f474417604bd383d2cd445d071b07dd55fedceed3ce33407bf1fcc107290", size = 2103683, upload-time = "2026-10-11T18:34:27.42Z" }, + { url = "https://files.pythonhosted.org/packages/5f/98/f9bd7e1f9b6709f155acb9ef826d9c3884fe925f811fd8e55b9b52280bad/pydantic_core-2.50.1-cp315-cp315-win_amd64.whl", hash = "sha256:983a662de2571cb2502fc8ff47b6770b03d025d2eb314c92f77b3f07c74720ed", size = 2171681, upload-time = "2026-10-11T18:34:29.508Z" }, + { url = "https://files.pythonhosted.org/packages/10/87/4bb3e1e7f385c076ab5af4d6dd0571b22042cd8207eb810d5b9fef15ae31/pydantic_core-2.50.1-cp315-cp315-win_arm64.whl", hash = "sha256:94845ff54dc5193f228cab81b2662a04bfbb892e95bdc15edf7399000ce57d54", size = 2156251, upload-time = "2026-10-11T18:34:31.455Z" }, + { url = "https://files.pythonhosted.org/packages/47/47/83643225b08f2aef6c8cc4bbe6e3f79c5e139c4364a6e450d0f399d87774/pydantic_core-2.50.1-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:4a53d13cdfbedbfa87f08b83c1a0a5efcc767d785a4b41934fa9cb672670493a", size = 2234363, upload-time = "2026-10-11T18:34:33.65Z" }, + { url = "https://files.pythonhosted.org/packages/5e/66/127ca649ba2f2462039dc1e694c6c01e00a3689f023a617364d3507e6d97/pydantic_core-2.50.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:efbecf43d321f7b9281441f1f213f7c21c66988b0e06c2730ba13ed47a46bb08", size = 2000972, upload-time = "2026-10-11T18:34:36.037Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4f/e421e0a5d653b1203b090b2e48745976988d7338a647940704b5b9c2b399/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc1f08f68dac9f9e83845a8039880aba2ab553eb9b2259c3243a313182c253fe", size = 2022925, upload-time = "2026-10-11T18:34:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/ea5568e2491e1a71100f15ae8c2d01ef52db42a184a2d4e716bc79e5eb8f/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5dfe41f232befddb9c4377f6cfc702b51595e2d78ed082672adf8758d2c4619f", size = 2192867, upload-time = "2026-10-11T18:34:39.921Z" }, + { url = "https://files.pythonhosted.org/packages/ae/5e/3b8c3a35acbe219909ada5defad5d7d9fed845fb2b37bd5ec518f453c119/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adc06d218a1cadfd2ec4628424d7d79ce4eba69c2965e7e7b55106f0da5208c8", size = 2388687, upload-time = "2026-10-11T18:34:42.184Z" }, + { url = "https://files.pythonhosted.org/packages/b6/aa/7889b4e515f91a2e8c0ae6b5081fec0feb30c4398d1434e14793c60f173a/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2cf91809d0721ab81592ba67bea7694821679c10b1a2e3c3460082b286c1918a", size = 2464415, upload-time = "2026-10-11T18:34:44.384Z" }, + { url = "https://files.pythonhosted.org/packages/08/78/93449e628eb8a6fdcce3eff9043081179d1bc7ce6f1bff32dc5006f41e00/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23923ab9292c40da026330b1ecf4dc2618c8e86e0422e5d1fbf50d94d64ca4f8", size = 2150823, upload-time = "2026-10-11T18:34:46.392Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f1/72c5bc129fceb0d00f05dc1e67f518c1728de1928c55f81fc13d7690de39/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:f3377c8c2b3ce898423c5e5dd94c7982e30aa7717a7e6ab2470b9de364963709", size = 2245806, upload-time = "2026-10-11T18:34:48.805Z" }, + { url = "https://files.pythonhosted.org/packages/28/2a/922a0e78f3aa6ab837b59f88190233fb8dad546c17995fde9bb3ed3b9b49/pydantic_core-2.50.1-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:455a773617b5913bf5c20d0692e5787b119e52c4d40ea644ca31f5758fd31be2", size = 2264160, upload-time = "2026-10-11T18:34:50.876Z" }, + { url = "https://files.pythonhosted.org/packages/52/a8/0f1449e3e1b20941c9372faa18e7b6a092e30cdfa841902473f7989532ac/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_aarch64.whl", hash = "sha256:1a9006395dece0e32e704c315eff8a00bede494f6108546cfc5539c89fef4f9a", size = 2294031, upload-time = "2026-10-11T18:34:52.876Z" }, + { url = "https://files.pythonhosted.org/packages/d1/d0/1031f492857de70355fb16524bbb03efce5fd34c93ed4a1ec60be07e0d4b/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_armv7l.whl", hash = "sha256:d2d82aa62521c55ddfb000ae70f88cdd8de974078f6024e821dfe5addd0c818f", size = 2468423, upload-time = "2026-10-11T18:34:54.995Z" }, + { url = "https://files.pythonhosted.org/packages/46/52/269ffffa645b8e47906395a39cf9db1151ae9fa4bcd7f47b960bc31baf37/pydantic_core-2.50.1-cp315-cp315t-musllinux_1_1_x86_64.whl", hash = "sha256:009634b83993777ddcd69cad0ffcace43dabde692109528e35f0fde91e386a8b", size = 2512598, upload-time = "2026-10-11T18:34:57.149Z" }, + { url = "https://files.pythonhosted.org/packages/31/5c/e47e28281f20326ff6f3c31d626f0a83e615d2d94ba41bb0ad6ec237184a/pydantic_core-2.50.1-cp315-cp315t-win32.whl", hash = "sha256:3fde4fdc6487a58d944ca87cf5adc95d5f266e872c19599f5f4c0a8a1b1f9f9f", size = 2098535, upload-time = "2026-10-11T18:34:59.313Z" }, + { url = "https://files.pythonhosted.org/packages/03/ad/759e181e69c1b472c60b2049e5f61d5da1deefdd5ce1df4bb2ebcf771f25/pydantic_core-2.50.1-cp315-cp315t-win_amd64.whl", hash = "sha256:1c8632d4ac04e6f91128fca584b3a8a507d81604c24eeaaad00d4be42765c32b", size = 2148645, upload-time = "2026-10-11T18:35:01.571Z" }, + { url = "https://files.pythonhosted.org/packages/6d/56/8a702c27e5be9f47e5f19d8669227424290e4c024e7c370279cbaf244b4e/pydantic_core-2.50.1-cp315-cp315t-win_arm64.whl", hash = "sha256:c3ede305158e75510be50869b319550ab072008c13d64d4ab1e094fb286b6f44", size = 2175297, upload-time = "2026-10-11T18:35:04.079Z" }, + { url = "https://files.pythonhosted.org/packages/e7/73/a23a327237d9bb985298ce47702e196f995ce850224c12c70886bd66bb5c/pydantic_core-2.50.1-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:062e891facce5ca296a1c37098e5e466780457f86413894b399f0cf22934f769", size = 2162560, upload-time = "2026-10-11T18:35:06.274Z" }, + { url = "https://files.pythonhosted.org/packages/df/b2/33b37b5e82408f402a5e82649a43e7c95cd5dab7beaeef2c35cedbfd18f2/pydantic_core-2.50.1-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:49c2cbb2397fe4d0987e84606e691af6cb87bc0ee1bd3e7b737f7e10b4c142f9", size = 2075250, upload-time = "2026-10-11T18:35:08.455Z" }, + { url = "https://files.pythonhosted.org/packages/04/62/c7d5b9249466ccab90f3ce4790c2399dbfb8172ae9e74a7753dd295fe65a/pydantic_core-2.50.1-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e4472072de0137ee0d8e72d6620e85939c271d2f90f6bbb4b15c24638b79f92", size = 2107891, upload-time = "2026-10-11T18:35:10.918Z" }, + { url = "https://files.pythonhosted.org/packages/d4/7e/28d29bc1657195933147f772dee0558d9e0f045bbc11db7f992f40c37bb5/pydantic_core-2.50.1-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ed4f3cef55164b026fefb41341b7754cc6b624c75dfe7142d2ecceb5ad21c87", size = 2268026, upload-time = "2026-10-11T18:35:13.336Z" }, + { url = "https://files.pythonhosted.org/packages/a5/09/1bcf160f3cd6333e2a76982fae5963e5032de0bcaaba38464223dc266bfe/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:76e2e83fa6ec8cdc972d438dafc2522b3a47bee4ec0ae668b29cfb1977ab5242", size = 2217070, upload-time = "2026-10-11T18:35:15.688Z" }, + { url = "https://files.pythonhosted.org/packages/3d/81/f02de95eec7794dfc7a5bc69a834f0e834b31b3009014ce6de903855f775/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:a51eee75939cf811ac09b278745a6cee7dc873ccfbc8b9af3cc88fe4b7ce25b5", size = 2039027, upload-time = "2026-10-11T18:35:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/4d/16/227746ed3771d9bf2304568b55134849ece34e13b38b1d76de763015c77f/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae28183297fb0d2b8dc46a1f01d51f5e45825fc5afe76a835a6cb7fb34821295", size = 2109009, upload-time = "2026-10-11T18:35:20.218Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b9/7664d1592a0e5a74903f357cd5b28ceb7336f3473256c5a4e8c8432f1881/pydantic_core-2.50.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88e492e8b9d0312e7dc13667c30222abf284dc3b79b5302b3607b41a5784ce61", size = 2269883, upload-time = "2026-10-11T18:35:22.48Z" }, + { url = "https://files.pythonhosted.org/packages/bd/35/002a378302ff91d2a7f149bc8d22363faf581ba40ac39d0123444315a32d/pydantic_core-2.50.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7456d699b13954e9c0164dcb267250a10ae0dfb03e6e26d6796ab0d46e189c84", size = 2154076, upload-time = "2026-10-11T18:35:24.743Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5c/7a034330082c00f3c9795052bd9b161aaaf6e1951e897ac00a094823ef7e/pydantic_core-2.50.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0d955195bbbe489ad343fcc956eacea9357b79cb22192c66cacdefcbc14b32f", size = 2085850, upload-time = "2026-10-11T18:35:27.009Z" }, + { url = "https://files.pythonhosted.org/packages/57/fa/d5b44a7d3d4eceed6ef8713a0003af56fefde6ec75915513bcdd5d730cd9/pydantic_core-2.50.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2c634642694e6a0dad2ab1d375589fa671fd442edd5caf7d9737b8f6ca22906", size = 2259754, upload-time = "2026-10-11T18:35:29.763Z" }, + { url = "https://files.pythonhosted.org/packages/b6/88/e01dd37301710bd8b3b2daf34ec9e2a6e83a333130831c0d468b0292a48c/pydantic_core-2.50.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6db2fbed51a7991302e8fac498cd67e336246026d0dfa84cf5166ce1412760", size = 2268712, upload-time = "2026-10-11T18:35:32.654Z" }, + { url = "https://files.pythonhosted.org/packages/3e/43/5bb80d4a4d2b2d84612a30205662cc521a20d209c94abc079ea48b4e0df2/pydantic_core-2.50.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:79490e33c4c0fcb933bbbcfc3a62184d8803b99f535863dfbb925e1bcb6945ad", size = 2305049, upload-time = "2026-10-11T18:35:35.392Z" }, + { url = "https://files.pythonhosted.org/packages/e8/7b/e3ef76b269cfe4a5bd3bdcf5d7124cf05e5a8909d443b91639307116bdae/pydantic_core-2.50.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:48569b0ade9edfbe065cad1d700175546592aebbb42f02adcebcc26e75b896fe", size = 2466001, upload-time = "2026-10-11T18:35:37.791Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7a/ea0947aadc1d9f72d4be00d2e19939611ec17363bcb5e6eb9ec7d8221c34/pydantic_core-2.50.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5f3cae32fc46121f787cb2486de9cf95a8bf72aec5cc78f64c606fa1735a6ef5", size = 2481777, upload-time = "2026-10-11T18:35:40.287Z" }, + { url = "https://files.pythonhosted.org/packages/24/f3/d15bc0b1fb0c4f1e07b3326ffb7489f7ae70da2b56a0884d22641b0eb47c/pydantic_core-2.50.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:7f476456ac2bb0d937f75191494a09c83a30765fea4f70f3b404942fe25f6cdf", size = 2336787, upload-time = "2026-10-11T18:35:42.558Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pyqwest" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/2a/f8634142207082b1c559a499451db794c96130e586f1edb22dc1da4b05b2/pyqwest-0.12.0.tar.gz", hash = "sha256:83ff498bd53bee92a2d2bf14476679307a99c78421c02cae08ec113bed2fbd7d", size = 516105, upload-time = "2026-10-09T06:22:44.949Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/e9/bf753f3001ca4cc72986eb9de68c08c1042c010a1dc2e93be32b0290e04d/pyqwest-0.12.0-cp311-abi3-macosx_10_12_x86_64.whl", hash = "sha256:03e8b74a4f6ad32759f0cee9c79d7bae37d3e8e346a17fe6795de992995b23fd", size = 5331355, upload-time = "2026-10-09T06:21:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/b1/a3/68e7e8a173a4b5f8a759d9a5a4a60c2feffe052969e9126f8a3448dc357e/pyqwest-0.12.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:c1af3aad4eef813efc43f40a6e4e2791643d42f9218b6f1e9f9bdaa2a19bdc7d", size = 5189665, upload-time = "2026-10-09T06:21:19.744Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e5/407de30d15456583af485488dcb62c99fa24bcac6fd1521b57ef848aac27/pyqwest-0.12.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dd55f27e4d526babb30c0d0e19c5517d7d6ecb59ab4219a30a5115fac1288698", size = 5751518, upload-time = "2026-10-09T06:21:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/29/9d/d7dcb2f3da4ea43c0e68894acea655063955d0c91401e74fb97f79c8265f/pyqwest-0.12.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:699662fca52a1595a1ed9c169816b7654c0b28308b9284091e96bbc79b8e408f", size = 5722887, upload-time = "2026-10-09T06:21:22.916Z" }, + { url = "https://files.pythonhosted.org/packages/28/dd/dee75b5c39132c49e24313eeb79caa6c178a1975578f7226aac4bed2dbe1/pyqwest-0.12.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1a8815ab7b61dc932379d42424e422a66df9634499095942e3b91bfd8f5e1dfd", size = 5904177, upload-time = "2026-10-09T06:21:24.768Z" }, + { url = "https://files.pythonhosted.org/packages/24/2c/4a75b193f01a70375e6af2e080a12a64fd2057d342f4cd71090adaf6b949/pyqwest-0.12.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:0d4828c8dfbfee936c951cc467f0197891a752eb0d22d7469f1adb02152c400a", size = 6095821, upload-time = "2026-10-09T06:21:26.781Z" }, + { url = "https://files.pythonhosted.org/packages/d9/da/b618b54b17ed111b0bfd3a89d74aa4477ca3cb35dfe73d5b0f9916c6c07d/pyqwest-0.12.0-cp311-abi3-win_amd64.whl", hash = "sha256:3958c9ea1316e9b71290006f044e9c020daec5b5b652629bae9d2b26b6d5294d", size = 4969600, upload-time = "2026-10-09T06:21:28.615Z" }, + { url = "https://files.pythonhosted.org/packages/1c/74/2ef8a770aa6152da60c201e091cd5c692a591641e1c485348caed8591015/pyqwest-0.12.0-cp311-abi3-win_arm64.whl", hash = "sha256:ab6f54ea17a34628f631ca7b1e010f0dd5891f75fd9b743afcbdf1d29740120d", size = 4943859, upload-time = "2026-10-09T06:21:30.151Z" }, + { url = "https://files.pythonhosted.org/packages/88/49/494866a18aecda4593a52ca62397d53fb29938033075d41c009291096fe6/pyqwest-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ba88e09d0cb6166b72a4a120a10df863f10c77dac8f53538a7ac266798e14167", size = 5344568, upload-time = "2026-10-09T06:21:31.89Z" }, + { url = "https://files.pythonhosted.org/packages/84/ee/09486016196b4a367286a8819e83185b78f771b01a3ca419e94d5c5589e6/pyqwest-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f2843c2f257db63580096f9bd4e83225a7c660a946693ec87777f906f5cdd21d", size = 5192763, upload-time = "2026-10-09T06:21:33.423Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0d/42cb6e771aabb8745ccc7184bd132e4471a8659cdd7bee65f05dc94f0a78/pyqwest-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:6319e35a3f867a1a57071cfe63364b9f35b38d4ae3a6231fcab5d3a82e9d5705", size = 5751522, upload-time = "2026-10-09T06:21:35.123Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a7/d09ef94ee9811ca7a698f2947a25618a60226c13f3e9193a2bc1880c4178/pyqwest-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ba001c5f06a92426635d86c2ccb8a4c732f2bb1424361c68161f8726b5141520", size = 5714017, upload-time = "2026-10-09T06:21:36.672Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b8/0770a8eaa97e845cbdf1e5fd9af36235580aae18f63d7e6ed0f257ffde01/pyqwest-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:39a7e94b109bc10b05a6151308bac6ead404a900768861216467544337a971d6", size = 5903886, upload-time = "2026-10-09T06:21:38.202Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/ad021859fb8380541604822ec6252c6e721f504b108e805b533ae8083a1f/pyqwest-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b8527b9795d7e72efaf0f2fcba6b293f85e1720201e44f94889ca33d263079e9", size = 6091049, upload-time = "2026-10-09T06:21:40.333Z" }, + { url = "https://files.pythonhosted.org/packages/0f/25/1167eab0e8fa4c07f8bbec5931749f7932d13bfea1ea3daa6dc78c67b8e1/pyqwest-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d9f952e66e848612f5222794112f203a51070f93ca7129c4df5d6c7ef8f4762c", size = 5351261, upload-time = "2026-10-09T06:21:41.989Z" }, + { url = "https://files.pythonhosted.org/packages/d2/10/29daf8cffe7c547121d29b96a5b46d59fbd8f21bd80826c54a28cd89bc75/pyqwest-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4371bd32687cac6cd31ae61a424b3c072450fb9e1d82ad568c234bc9935e2db8", size = 5195343, upload-time = "2026-10-09T06:21:43.773Z" }, + { url = "https://files.pythonhosted.org/packages/bc/25/905f500e78216a729d69d24ccdf7f7a4c722321755dac97825620a98a366/pyqwest-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:469ac955bccf7080c6ed92afdd023c633d6d704b4be0653b82741fda3a60c85b", size = 5753112, upload-time = "2026-10-09T06:21:45.194Z" }, + { url = "https://files.pythonhosted.org/packages/34/93/b12629c268e743ed6cf39a08a78a696cbdb10d910fbcf4b0de6b0f89f143/pyqwest-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:05af8d0c6eb67661d6896824183b714a908321b3d10644594268b51d21a9cb1c", size = 5713673, upload-time = "2026-10-09T06:21:46.687Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c3/b0f689e60356143661c93e7295edd959a738e0d42a101e4c3647045d23c9/pyqwest-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5f1f6599c0f826fd7e6717a3c6709d9fc25e1debc9e70bbb4b613c25377937c8", size = 5905698, upload-time = "2026-10-09T06:21:48.297Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d6/412b69950180abf077c2dbf0b8ddf425a5fd65466e775bfbd63e28aefd1a/pyqwest-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d4de6b62293d57153db308897988afdfd22e481c4399279c6332c9efbc183330", size = 6090438, upload-time = "2026-10-09T06:21:50.131Z" }, + { url = "https://files.pythonhosted.org/packages/de/c0/1f6fca89081737afd8842bf6b4a6e6014dedd8d7d44b4da2d73f502c3b4c/pyqwest-0.12.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:34399a6b4c2d03ece732d41ed051710ea35c70f7daed9590ce9916db6cde92e6", size = 5336542, upload-time = "2026-10-09T06:21:51.839Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0f/7f364ac04e01258e0cfc37170a558cb36aaacff110f1a4c9d2f887e64be9/pyqwest-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1335062111f4da599d58223eeeb051f839a2c588274258facf6091aca92201a3", size = 5174602, upload-time = "2026-10-09T06:21:53.489Z" }, + { url = "https://files.pythonhosted.org/packages/11/9f/cb9c8ad10dc43e0b854041f4b471fde59624e8407c9c6da00f8907933c57/pyqwest-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:218f7a00d01678c7f5f02a0248d3b601d87d9f882179305f0c8f0545eb88d733", size = 5731531, upload-time = "2026-10-09T06:21:55.061Z" }, + { url = "https://files.pythonhosted.org/packages/52/2e/30a2b41d340d07f8205d02997a24c518c031d09898c8004ad671a8c9bc8b/pyqwest-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:27843d48330e2a163cf875e05294d9e4ea0e0e91bdbea956e3af630b678bd7d8", size = 5704167, upload-time = "2026-10-09T06:21:56.687Z" }, + { url = "https://files.pythonhosted.org/packages/60/c3/8c870cbf47857abd8490b8da2195cad71c3890556d776ecc01cd744bb35c/pyqwest-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4cdb0f156e754caead9b8cf88fc2f2806ed81282960c9a39f8a07847110745fe", size = 5882872, upload-time = "2026-10-09T06:21:58.362Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e4/45017cda255c95797c5a4e6da2f87a8035f368aa9db50d9c45abcbd97387/pyqwest-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3208f670e601ddfbf6ff689c7717236dfeaf175fa934319d30a6c57df0e1c14c", size = 6074173, upload-time = "2026-10-09T06:21:59.918Z" }, + { url = "https://files.pythonhosted.org/packages/a3/d1/955421179cbd93070cd01769e6f937826c42d377fab9fade00172601e138/pyqwest-0.12.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:f7476abcdac195e33165e38e4e76c9c6e90785e14033b766912f24ff8f829198", size = 5398661, upload-time = "2026-10-09T06:22:01.5Z" }, + { url = "https://files.pythonhosted.org/packages/e0/b0/c7474b1650d3bafe94f28f87446b0e4371d553acdfa79d2ca7e8151d1982/pyqwest-0.12.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:faf377620445e9988b2e290fb805dd3a04ffa62e0f6d1da11e269cecc1d5a837", size = 5241423, upload-time = "2026-10-09T06:22:03.084Z" }, + { url = "https://files.pythonhosted.org/packages/b8/11/3bdeb82f4425ed42d5ee877003974de7a716990f8681c6a637046a3d49eb/pyqwest-0.12.0-cp315-abi3.abi3t-manylinux_2_28_aarch64.whl", hash = "sha256:90fee9cd448324fbdc4ed2dff787749f19ac17315aa9e18d88ced0b843fc8dac", size = 5791319, upload-time = "2026-10-09T06:22:04.628Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b5/024555a08ab26bdadbe63b7acb6115a5d0e536efa94c4c71c4bbce445a45/pyqwest-0.12.0-cp315-abi3.abi3t-manylinux_2_28_x86_64.whl", hash = "sha256:ae7b73b47e3fb2462a5292417d49e7780ce2dc9857e8bce7276ea2f090a90768", size = 5764278, upload-time = "2026-10-09T06:22:06.309Z" }, + { url = "https://files.pythonhosted.org/packages/61/44/57bba169ee17c02dfd0b2a6508ed4c027e0d14ee3765fa3c641399521e2d/pyqwest-0.12.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:5f83e15b7bebc761e7b6a0f1cbac334d058e698fb49ba1328306660b00c3c92d", size = 5939856, upload-time = "2026-10-09T06:22:07.915Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/926b90a270e9893958840fa610531b5a0d47604e088e1c60305c13890938/pyqwest-0.12.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:8057eddcae424eadbf15c9ab67f4aebdc2ce5f2cf1d8d4f6c34ca9d9ab62fef5", size = 6139873, upload-time = "2026-10-09T06:22:09.498Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6c/a09b970067a60c721ad063107ac10917e31475211bb3d66995d1482331a2/pyqwest-0.12.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:6c71f9f32cd344ac4f136010ea6a307201547123bce6ba61272421c93fb4fc6d", size = 5023514, upload-time = "2026-10-09T06:22:11.179Z" }, + { url = "https://files.pythonhosted.org/packages/d7/e5/06c6540c29596fe9230ccc160822309592b1ec85a05482105e1de6b98d14/pyqwest-0.12.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:9ca7d29a469326604f7c048bc19d4592970fbc7ac494bc253ef0779a57e1a9aa", size = 4991803, upload-time = "2026-10-09T06:22:12.867Z" }, + { url = "https://files.pythonhosted.org/packages/fb/4e/3f4e924e430cd859df54395b7a9ef21c342f544dc4d4da3af5b3f44c1e08/pyqwest-0.12.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:dca14e8c4aa3d79601c91e408e33df0bf463c8166539b394715637f49f90362a", size = 5348621, upload-time = "2026-10-09T06:22:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/90/f2/da5defcc9d231ab9e4ce267704d1e18544b4d93c1cfb4e3c29a9a35892d3/pyqwest-0.12.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:729a81368abcbe6be708a96da1ec8802969082ffaab35d1e08a520e3c0453a49", size = 5195352, upload-time = "2026-10-09T06:22:16.435Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/871c99fdedaff6458a519e37d49400617beebf9c6560ed5e73bf02b972fe/pyqwest-0.12.0-cp315-cp315-manylinux_2_28_aarch64.whl", hash = "sha256:645072f8e9138556e27f6c9384dd1fe0bdef3f1be50534d2c1d9ffeea9a53de9", size = 5752432, upload-time = "2026-10-09T06:22:18.111Z" }, + { url = "https://files.pythonhosted.org/packages/0c/4e/d1686f7b74c2b5cb3f7939a397aa215054db3cb515a99503fe6e361ddc96/pyqwest-0.12.0-cp315-cp315-manylinux_2_28_x86_64.whl", hash = "sha256:d302f7bf0c072397aafe2cbe6a398743e5c37add3500048bbe0b26bba823075f", size = 5714200, upload-time = "2026-10-09T06:22:19.542Z" }, + { url = "https://files.pythonhosted.org/packages/6f/15/d4a71b8b9c85f74174f3e58ebe6cc34cf2d9175424da84af91088521b74f/pyqwest-0.12.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:ead61088e5226349624965025d297ed893dd947e05ccf315006322c824ef9a4e", size = 5903794, upload-time = "2026-10-09T06:22:21.083Z" }, + { url = "https://files.pythonhosted.org/packages/f4/b1/01928c04841f525748b4b21e46e29a473cc1b9d2c800abcf70d5d6a12b56/pyqwest-0.12.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:db85298d530bcd50234c48dfd18facbe75a3c8f0f16e04ace389617d48a3093f", size = 6091500, upload-time = "2026-10-09T06:22:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ac/181acdc359a7cc293a4fe579ac315e72676654bd6f5ead732304caa125d0/pyqwest-0.12.0-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:f87b16a83d8e0bc5b639163819c53a9e975ea2fbeec133ecbd2ffce0796dab27", size = 5336607, upload-time = "2026-10-09T06:22:24.437Z" }, + { url = "https://files.pythonhosted.org/packages/74/b1/7365631700810ec294119e3b405bd20d9897774979b021c5dc2c002e4773/pyqwest-0.12.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:fa846050a669d0eacc2c2aa425eda403b3e443a374ff84c5c16c9f82624a2a9d", size = 5174497, upload-time = "2026-10-09T06:22:25.915Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/08bc79d4d1c3af8c24b78163c277226cc26c3e751d1bc562db8baf67ee7e/pyqwest-0.12.0-cp315-cp315t-manylinux_2_28_aarch64.whl", hash = "sha256:5f08aeda136ffcb454c11b1e7bb98a85853ea779424cc30e98a9e5e762b84fea", size = 5731691, upload-time = "2026-10-09T06:22:27.408Z" }, + { url = "https://files.pythonhosted.org/packages/39/65/e1a9cc3cf610122149ccd3a1e264ce6b47aa8bcf3d4abd5f27ba7acdd97f/pyqwest-0.12.0-cp315-cp315t-manylinux_2_28_x86_64.whl", hash = "sha256:0b770869bae9e15c13e8823ed71155d2a9e7fa3f4cba3da65d2b4b668e5b3a1e", size = 5704613, upload-time = "2026-10-09T06:22:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/c4/75/a8b3fd3de3eab08377ae71fde7efa5526b7f43cf094eb8b028dd8acee275/pyqwest-0.12.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:a25bdfbdc859d937de818db60d839f523c9d4792c2b85a23667861cc0791246e", size = 5886056, upload-time = "2026-10-09T06:22:31.298Z" }, + { url = "https://files.pythonhosted.org/packages/93/23/6982518b22268151fdb7ae27d3700a3c4fdb0699d4783caaace1e1fa6deb/pyqwest-0.12.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ac6a7de3327ee56e736108ab72a21efc5dfddd41ce4c19e4469c1e8448a6696e", size = 6074658, upload-time = "2026-10-09T06:22:32.919Z" }, + { url = "https://files.pythonhosted.org/packages/0d/78/31357c37d50ad3de0c8c0145908cef4d06128c3cc0cb6a8373d5bf45165b/pyqwest-0.12.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d8e757161f3c812c27b885a0a6334aa12b98a113a0f1164725175420bdc451f5", size = 5332397, upload-time = "2026-10-09T06:22:34.399Z" }, + { url = "https://files.pythonhosted.org/packages/d0/83/8c9439e0f95b5d52566575cba4dbd8923e97eec2e511a6ab3c4a6ec59608/pyqwest-0.12.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4579b040fb22226473741c5c901fae86655a695451eb4bfc399b758ca8370420", size = 5193341, upload-time = "2026-10-09T06:22:35.987Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c9/bb1ad907a66d52c097bedc1c2c1bf7830566bca88e24aeb78ab2d34db6ec/pyqwest-0.12.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:cc9fa48ba09dd96568c1c46eaffda61809d66ef4013957b2a70152a802b19e34", size = 5750909, upload-time = "2026-10-09T06:22:37.675Z" }, + { url = "https://files.pythonhosted.org/packages/f3/41/0a6d6c72828881ed396131a4de45037144b036412c3e5353285052606968/pyqwest-0.12.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:38811c3d6a7cfacf9b013c6fe0d948f668d247896edc9545df7ca0b02de64360", size = 5719293, upload-time = "2026-10-09T06:22:39.415Z" }, + { url = "https://files.pythonhosted.org/packages/5e/05/91f6fb96a02d97ba9663092c50a6f28797e06aa853245bf9283cc4637144/pyqwest-0.12.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e5d634ef3a5c569a3762c3393c297f9d5be618a7506c021238a263da80b1d9d0", size = 5904717, upload-time = "2026-10-09T06:22:41.161Z" }, + { url = "https://files.pythonhosted.org/packages/df/93/8bc6380d1c25729d5872c90225607892f6a491db0a01dad28cccd0967b04/pyqwest-0.12.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dcef82a96cdfb1946b0b0705e41019eb077e498dc3ce5ade52a962d338f51ca6", size = 6094579, upload-time = "2026-10-09T06:22:42.86Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, +] + +[[package]] +name = "pytest-dotenv" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/b0/cafee9c627c1bae228eb07c9977f679b3a7cb111b488307ab9594ba9e4da/pytest-dotenv-0.5.2.tar.gz", hash = "sha256:2dc6c3ac6d8764c71c6d2804e902d0ff810fa19692e95fe138aefc9b1aa73732", size = 3782, upload-time = "2020-06-16T12:38:03.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/da/9da67c67b3d0963160e3d2cbc7c38b6fae342670cc8e6d5936644b2cf944/pytest_dotenv-0.5.2-py3-none-any.whl", hash = "sha256:40a2cece120a213898afaa5407673f6bd924b1fa7eafce6bda0e8abffe2f710f", size = 3993, upload-time = "2020-06-16T12:38:01.139Z" }, +] + +[[package]] +name = "pytest-timeout" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/26/2fbeedb218a787a5eea551c7532cac4e009f83d689dd2faa0d0353473f86/python_dotenv-1.2.4.tar.gz", hash = "sha256:f0d53e69935a851c0dcc78f3ab7aaccd8cabef0b92382b576b824212902873c0", size = 60824, upload-time = "2026-10-01T05:36:10Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/d1/38f3a3405989a89ac18390803e70c6ad7c7760da4f9b83cbeca0c44a0c72/python_dotenv-1.2.4-py3-none-any.whl", hash = "sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc", size = 23266, upload-time = "2026-10-01T05:36:08.633Z" }, +] + +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, + { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" }, + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "ruamel-yaml" +version = "0.18.17" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ruamel-yaml-clib", marker = "python_full_version < '3.15' and platform_python_implementation == 'CPython'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/2b/7a1f1ebcd6b3f14febdc003e658778d81e76b40df2267904ee6b13f0c5c6/ruamel_yaml-0.18.17.tar.gz", hash = "sha256:9091cd6e2d93a3a4b157ddb8fabf348c3de7f1fb1381346d985b6b247dcd8d3c", size = 149602, upload-time = "2025-12-17T20:02:55.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/fe/b6045c782f1fd1ae317d2a6ca1884857ce5c20f59befe6ab25a8603c43a7/ruamel_yaml-0.18.17-py3-none-any.whl", hash = "sha256:9c8ba9eb3e793efdf924b60d521820869d5bf0cb9c6f1b82d82de8295e290b9d", size = 121594, upload-time = "2025-12-17T20:02:07.657Z" }, +] + +[[package]] +name = "ruamel-yaml-clib" +version = "0.2.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/97/60fda20e2fb54b83a61ae14648b0817c8f5d84a3821e40bfbdae1437026a/ruamel_yaml_clib-0.2.15.tar.gz", hash = "sha256:46e4cc8c43ef6a94885f72512094e482114a8a706d3c555a34ed4b0d20200600", size = 225794, upload-time = "2025-11-16T16:12:59.761Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/80/8ce7b9af532aa94dd83360f01ce4716264db73de6bc8efd22c32341f6658/ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c583229f336682b7212a43d2fa32c30e643d3076178fb9f7a6a14dde85a2d8bd", size = 147998, upload-time = "2025-11-16T16:13:13.241Z" }, + { url = "https://files.pythonhosted.org/packages/53/09/de9d3f6b6701ced5f276d082ad0f980edf08ca67114523d1b9264cd5e2e0/ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56ea19c157ed8c74b6be51b5fa1c3aff6e289a041575f0556f66e5fb848bb137", size = 132743, upload-time = "2025-11-16T16:13:14.265Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f7/73a9b517571e214fe5c246698ff3ed232f1ef863c8ae1667486625ec688a/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5fea0932358e18293407feb921d4f4457db837b67ec1837f87074667449f9401", size = 731459, upload-time = "2025-11-16T20:22:44.338Z" }, + { url = "https://files.pythonhosted.org/packages/9b/a2/0dc0013169800f1c331a6f55b1282c1f4492a6d32660a0cf7b89e6684919/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71831bd61fbdb7aa0399d5c4da06bea37107ab5c79ff884cc07f2450910262", size = 749289, upload-time = "2025-11-16T16:13:15.633Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ed/3fb20a1a96b8dc645d88c4072df481fe06e0289e4d528ebbdcc044ebc8b3/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:617d35dc765715fa86f8c3ccdae1e4229055832c452d4ec20856136acc75053f", size = 777630, upload-time = "2025-11-16T16:13:16.898Z" }, + { url = "https://files.pythonhosted.org/packages/60/50/6842f4628bc98b7aa4733ab2378346e1441e150935ad3b9f3c3c429d9408/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b45498cc81a4724a2d42273d6cfc243c0547ad7c6b87b4f774cb7bcc131c98d", size = 744368, upload-time = "2025-11-16T16:13:18.117Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b0/128ae8e19a7d794c2e36130a72b3bb650ce1dd13fb7def6cf10656437dcf/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:def5663361f6771b18646620fca12968aae730132e104688766cf8a3b1d65922", size = 745233, upload-time = "2025-11-16T20:22:45.833Z" }, + { url = "https://files.pythonhosted.org/packages/75/05/91130633602d6ba7ce3e07f8fc865b40d2a09efd4751c740df89eed5caf9/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:014181cdec565c8745b7cbc4de3bf2cc8ced05183d986e6d1200168e5bb59490", size = 770963, upload-time = "2025-11-16T16:13:19.344Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4b/fd4542e7f33d7d1bc64cc9ac9ba574ce8cf145569d21f5f20133336cdc8c/ruamel_yaml_clib-0.2.15-cp311-cp311-win32.whl", hash = "sha256:d290eda8f6ada19e1771b54e5706b8f9807e6bb08e873900d5ba114ced13e02c", size = 102640, upload-time = "2025-11-16T16:13:20.498Z" }, + { url = "https://files.pythonhosted.org/packages/bb/eb/00ff6032c19c7537371e3119287999570867a0eafb0154fccc80e74bf57a/ruamel_yaml_clib-0.2.15-cp311-cp311-win_amd64.whl", hash = "sha256:bdc06ad71173b915167702f55d0f3f027fc61abd975bd308a0968c02db4a4c3e", size = 121996, upload-time = "2025-11-16T16:13:21.855Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/5fde11a0722d676e469d3d6f78c6a17591b9c7e0072ca359801c4bd17eee/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb15a2e2a90c8475df45c0949793af1ff413acfb0a716b8b94e488ea95ce7cff", size = 149088, upload-time = "2025-11-16T16:13:22.836Z" }, + { url = "https://files.pythonhosted.org/packages/85/82/4d08ac65ecf0ef3b046421985e66301a242804eb9a62c93ca3437dc94ee0/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64da03cbe93c1e91af133f5bec37fd24d0d4ba2418eaf970d7166b0a26a148a2", size = 134553, upload-time = "2025-11-16T16:13:24.151Z" }, + { url = "https://files.pythonhosted.org/packages/b9/cb/22366d68b280e281a932403b76da7a988108287adff2bfa5ce881200107a/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f6d3655e95a80325b84c4e14c080b2470fe4f33b6846f288379ce36154993fb1", size = 737468, upload-time = "2025-11-16T20:22:47.335Z" }, + { url = "https://files.pythonhosted.org/packages/71/73/81230babf8c9e33770d43ed9056f603f6f5f9665aea4177a2c30ae48e3f3/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71845d377c7a47afc6592aacfea738cc8a7e876d586dfba814501d8c53c1ba60", size = 753349, upload-time = "2025-11-16T16:13:26.269Z" }, + { url = "https://files.pythonhosted.org/packages/61/62/150c841f24cda9e30f588ef396ed83f64cfdc13b92d2f925bb96df337ba9/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e5499db1ccbc7f4b41f0565e4f799d863ea720e01d3e99fa0b7b5fcd7802c9", size = 788211, upload-time = "2025-11-16T16:13:27.441Z" }, + { url = "https://files.pythonhosted.org/packages/30/93/e79bd9cbecc3267499d9ead919bd61f7ddf55d793fb5ef2b1d7d92444f35/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4b293a37dc97e2b1e8a1aec62792d1e52027087c8eea4fc7b5abd2bdafdd6642", size = 743203, upload-time = "2025-11-16T16:13:28.671Z" }, + { url = "https://files.pythonhosted.org/packages/8d/06/1eb640065c3a27ce92d76157f8efddb184bd484ed2639b712396a20d6dce/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:512571ad41bba04eac7268fe33f7f4742210ca26a81fe0c75357fa682636c690", size = 747292, upload-time = "2025-11-16T20:22:48.584Z" }, + { url = "https://files.pythonhosted.org/packages/a5/21/ee353e882350beab65fcc47a91b6bdc512cace4358ee327af2962892ff16/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5e9f630c73a490b758bf14d859a39f375e6999aea5ddd2e2e9da89b9953486a", size = 771624, upload-time = "2025-11-16T16:13:29.853Z" }, + { url = "https://files.pythonhosted.org/packages/57/34/cc1b94057aa867c963ecf9ea92ac59198ec2ee3a8d22a126af0b4d4be712/ruamel_yaml_clib-0.2.15-cp312-cp312-win32.whl", hash = "sha256:f4421ab780c37210a07d138e56dd4b51f8642187cdfb433eb687fe8c11de0144", size = 100342, upload-time = "2025-11-16T16:13:31.067Z" }, + { url = "https://files.pythonhosted.org/packages/b3/e5/8925a4208f131b218f9a7e459c0d6fcac8324ae35da269cb437894576366/ruamel_yaml_clib-0.2.15-cp312-cp312-win_amd64.whl", hash = "sha256:2b216904750889133d9222b7b873c199d48ecbb12912aca78970f84a5aa1a4bc", size = 119013, upload-time = "2025-11-16T16:13:32.164Z" }, + { url = "https://files.pythonhosted.org/packages/17/5e/2f970ce4c573dc30c2f95825f2691c96d55560268ddc67603dc6ea2dd08e/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dcec721fddbb62e60c2801ba08c87010bd6b700054a09998c4d09c08147b8fb", size = 147450, upload-time = "2025-11-16T16:13:33.542Z" }, + { url = "https://files.pythonhosted.org/packages/d6/03/a1baa5b94f71383913f21b96172fb3a2eb5576a4637729adbf7cd9f797f8/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:65f48245279f9bb301d1276f9679b82e4c080a1ae25e679f682ac62446fac471", size = 133139, upload-time = "2025-11-16T16:13:34.587Z" }, + { url = "https://files.pythonhosted.org/packages/dc/19/40d676802390f85784235a05788fd28940923382e3f8b943d25febbb98b7/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46895c17ead5e22bea5e576f1db7e41cb273e8d062c04a6a49013d9f60996c25", size = 731474, upload-time = "2025-11-16T20:22:49.934Z" }, + { url = "https://files.pythonhosted.org/packages/ce/bb/6ef5abfa43b48dd55c30d53e997f8f978722f02add61efba31380d73e42e/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3eb199178b08956e5be6288ee0b05b2fb0b5c1f309725ad25d9c6ea7e27f962a", size = 748047, upload-time = "2025-11-16T16:13:35.633Z" }, + { url = "https://files.pythonhosted.org/packages/ff/5d/e4f84c9c448613e12bd62e90b23aa127ea4c46b697f3d760acc32cb94f25/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d1032919280ebc04a80e4fb1e93f7a738129857eaec9448310e638c8bccefcf", size = 782129, upload-time = "2025-11-16T16:13:36.781Z" }, + { url = "https://files.pythonhosted.org/packages/de/4b/e98086e88f76c00c88a6bcf15eae27a1454f661a9eb72b111e6bbb69024d/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab0df0648d86a7ecbd9c632e8f8d6b21bb21b5fc9d9e095c796cacf32a728d2d", size = 736848, upload-time = "2025-11-16T16:13:37.952Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5c/5964fcd1fd9acc53b7a3a5d9a05ea4f95ead9495d980003a557deb9769c7/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:331fb180858dd8534f0e61aa243b944f25e73a4dae9962bd44c46d1761126bbf", size = 741630, upload-time = "2025-11-16T20:22:51.718Z" }, + { url = "https://files.pythonhosted.org/packages/07/1e/99660f5a30fceb58494598e7d15df883a07292346ef5696f0c0ae5dee8c6/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd4c928ddf6bce586285daa6d90680b9c291cfd045fc40aad34e445d57b1bf51", size = 766619, upload-time = "2025-11-16T16:13:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/36/2f/fa0344a9327b58b54970e56a27b32416ffbcfe4dcc0700605516708579b2/ruamel_yaml_clib-0.2.15-cp313-cp313-win32.whl", hash = "sha256:bf0846d629e160223805db9fe8cc7aec16aaa11a07310c50c8c7164efa440aec", size = 100171, upload-time = "2025-11-16T16:13:40.456Z" }, + { url = "https://files.pythonhosted.org/packages/06/c4/c124fbcef0684fcf3c9b72374c2a8c35c94464d8694c50f37eef27f5a145/ruamel_yaml_clib-0.2.15-cp313-cp313-win_amd64.whl", hash = "sha256:45702dfbea1420ba3450bb3dd9a80b33f0badd57539c6aac09f42584303e0db6", size = 118845, upload-time = "2025-11-16T16:13:41.481Z" }, + { url = "https://files.pythonhosted.org/packages/3e/bd/ab8459c8bb759c14a146990bf07f632c1cbec0910d4853feeee4be2ab8bb/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:753faf20b3a5906faf1fc50e4ddb8c074cb9b251e00b14c18b28492f933ac8ef", size = 147248, upload-time = "2025-11-16T16:13:42.872Z" }, + { url = "https://files.pythonhosted.org/packages/69/f2/c4cec0a30f1955510fde498aac451d2e52b24afdbcb00204d3a951b772c3/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:480894aee0b29752560a9de46c0e5f84a82602f2bc5c6cde8db9a345319acfdf", size = 133764, upload-time = "2025-11-16T16:13:43.932Z" }, + { url = "https://files.pythonhosted.org/packages/82/c7/2480d062281385a2ea4f7cc9476712446e0c548cd74090bff92b4b49e898/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d3b58ab2454b4747442ac76fab66739c72b1e2bb9bd173d7694b9f9dbc9c000", size = 730537, upload-time = "2025-11-16T20:22:52.918Z" }, + { url = "https://files.pythonhosted.org/packages/75/08/e365ee305367559f57ba6179d836ecc3d31c7d3fdff2a40ebf6c32823a1f/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bfd309b316228acecfa30670c3887dcedf9b7a44ea39e2101e75d2654522acd4", size = 746944, upload-time = "2025-11-16T16:13:45.338Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5c/8b56b08db91e569d0a4fbfa3e492ed2026081bdd7e892f63ba1c88a2f548/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2812ff359ec1f30129b62372e5f22a52936fac13d5d21e70373dbca5d64bb97c", size = 778249, upload-time = "2025-11-16T16:13:46.871Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1d/70dbda370bd0e1a92942754c873bd28f513da6198127d1736fa98bb2a16f/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7e74ea87307303ba91073b63e67f2c667e93f05a8c63079ee5b7a5c8d0d7b043", size = 737140, upload-time = "2025-11-16T16:13:48.349Z" }, + { url = "https://files.pythonhosted.org/packages/5b/87/822d95874216922e1120afb9d3fafa795a18fdd0c444f5c4c382f6dac761/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:713cd68af9dfbe0bb588e144a61aad8dcc00ef92a82d2e87183ca662d242f524", size = 741070, upload-time = "2025-11-16T20:22:54.151Z" }, + { url = "https://files.pythonhosted.org/packages/b9/17/4e01a602693b572149f92c983c1f25bd608df02c3f5cf50fd1f94e124a59/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:542d77b72786a35563f97069b9379ce762944e67055bea293480f7734b2c7e5e", size = 765882, upload-time = "2025-11-16T16:13:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/9f/17/7999399081d39ebb79e807314de6b611e1d1374458924eb2a489c01fc5ad/ruamel_yaml_clib-0.2.15-cp314-cp314-win32.whl", hash = "sha256:424ead8cef3939d690c4b5c85ef5b52155a231ff8b252961b6516ed7cf05f6aa", size = 102567, upload-time = "2025-11-16T16:13:50.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/67/be582a7370fdc9e6846c5be4888a530dcadd055eef5b932e0e85c33c7d73/ruamel_yaml_clib-0.2.15-cp314-cp314-win_amd64.whl", hash = "sha256:ac9b8d5fa4bb7fd2917ab5027f60d4234345fd366fe39aa711d5dca090aa1467", size = 122847, upload-time = "2025-11-16T16:13:51.807Z" }, +] + +[[package]] +name = "ruff" +version = "0.11.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/da/9c6f995903b4d9474b39da91d2d626659af3ff1eeb43e9ae7c119349dba6/ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514", size = 4282054, upload-time = "2025-06-05T21:00:15.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/ce/a11d381192966e0b4290842cc8d4fac7dc9214ddf627c11c1afff87da29b/ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46", size = 10292516, upload-time = "2025-06-05T20:59:32.944Z" }, + { url = "https://files.pythonhosted.org/packages/78/db/87c3b59b0d4e753e40b6a3b4a2642dfd1dcaefbff121ddc64d6c8b47ba00/ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48", size = 11106083, upload-time = "2025-06-05T20:59:37.03Z" }, + { url = "https://files.pythonhosted.org/packages/77/79/d8cec175856ff810a19825d09ce700265f905c643c69f45d2b737e4a470a/ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b", size = 10436024, upload-time = "2025-06-05T20:59:39.741Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5b/f6d94f2980fa1ee854b41568368a2e1252681b9238ab2895e133d303538f/ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a", size = 10646324, upload-time = "2025-06-05T20:59:42.185Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9c/b4c2acf24ea4426016d511dfdc787f4ce1ceb835f3c5fbdbcb32b1c63bda/ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc", size = 10174416, upload-time = "2025-06-05T20:59:44.319Z" }, + { url = "https://files.pythonhosted.org/packages/f3/10/e2e62f77c65ede8cd032c2ca39c41f48feabedb6e282bfd6073d81bb671d/ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629", size = 11724197, upload-time = "2025-06-05T20:59:46.935Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f0/466fe8469b85c561e081d798c45f8a1d21e0b4a5ef795a1d7f1a9a9ec182/ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933", size = 12511615, upload-time = "2025-06-05T20:59:49.534Z" }, + { url = "https://files.pythonhosted.org/packages/17/0e/cefe778b46dbd0cbcb03a839946c8f80a06f7968eb298aa4d1a4293f3448/ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165", size = 12117080, upload-time = "2025-06-05T20:59:51.654Z" }, + { url = "https://files.pythonhosted.org/packages/5d/2c/caaeda564cbe103bed145ea557cb86795b18651b0f6b3ff6a10e84e5a33f/ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71", size = 11326315, upload-time = "2025-06-05T20:59:54.469Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/782e7d681d660eda8c536962920c41309e6dd4ebcea9a2714ed5127d44bd/ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9", size = 11555640, upload-time = "2025-06-05T20:59:56.986Z" }, + { url = "https://files.pythonhosted.org/packages/5d/d4/3d580c616316c7f07fb3c99dbecfe01fbaea7b6fd9a82b801e72e5de742a/ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc", size = 10507364, upload-time = "2025-06-05T20:59:59.154Z" }, + { url = "https://files.pythonhosted.org/packages/5a/dc/195e6f17d7b3ea6b12dc4f3e9de575db7983db187c378d44606e5d503319/ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7", size = 10141462, upload-time = "2025-06-05T21:00:01.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/8e/39a094af6967faa57ecdeacb91bedfb232474ff8c3d20f16a5514e6b3534/ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432", size = 11121028, upload-time = "2025-06-05T21:00:04.06Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c0/b0b508193b0e8a1654ec683ebab18d309861f8bd64e3a2f9648b80d392cb/ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492", size = 11602992, upload-time = "2025-06-05T21:00:06.249Z" }, + { url = "https://files.pythonhosted.org/packages/7c/91/263e33ab93ab09ca06ce4f8f8547a858cc198072f873ebc9be7466790bae/ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250", size = 10474944, upload-time = "2025-06-05T21:00:08.459Z" }, + { url = "https://files.pythonhosted.org/packages/46/f4/7c27734ac2073aae8efb0119cae6931b6fb48017adf048fdf85c19337afc/ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3", size = 11548669, upload-time = "2025-06-05T21:00:11.147Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bf/b273dd11673fed8a6bd46032c0ea2a04b2ac9bfa9c628756a5856ba113b0/ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b", size = 10683928, upload-time = "2025-06-05T21:00:13.758Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "ty" +version = "0.0.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/25/257602d316b9333089b688a7a11b33ebc660b74e8dacf400dc3dfdea1594/ty-0.0.15.tar.gz", hash = "sha256:4f9a5b8df208c62dba56e91b93bed8b5bb714839691b8cff16d12c983bfa1174", size = 5101936, upload-time = "2026-02-05T01:06:34.922Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/c5/35626e732b79bf0e6213de9f79aff59b5f247c0a1e3ce0d93e675ab9b728/ty-0.0.15-py3-none-linux_armv6l.whl", hash = "sha256:68e092458516c61512dac541cde0a5e4e5842df00b4e81881ead8f745ddec794", size = 10138374, upload-time = "2026-02-05T01:07:03.804Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8a/48fd81664604848f79d03879b3ca3633762d457a069b07e09fb1b87edd6e/ty-0.0.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:79f2e75289eae3cece94c51118b730211af4ba5762906f52a878041b67e54959", size = 9947858, upload-time = "2026-02-05T01:06:47.453Z" }, + { url = "https://files.pythonhosted.org/packages/b6/85/c1ac8e97bcd930946f4c94db85b675561d590b4e72703bf3733419fc3973/ty-0.0.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:112a7b26e63e48cc72c8c5b03227d1db280cfa57a45f2df0e264c3a016aa8c3c", size = 9443220, upload-time = "2026-02-05T01:06:44.98Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d9/244bc02599d950f7a4298fbc0c1b25cc808646b9577bdf7a83470b2d1cec/ty-0.0.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71f62a2644972975a657d9dc867bf901235cde51e8d24c20311067e7afd44a56", size = 9949976, upload-time = "2026-02-05T01:07:01.515Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ab/3a0daad66798c91a33867a3ececf17d314ac65d4ae2bbbd28cbfde94da63/ty-0.0.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9e48b42be2d257317c85b78559233273b655dd636fc61e7e1d69abd90fd3cba4", size = 9965918, upload-time = "2026-02-05T01:06:54.283Z" }, + { url = "https://files.pythonhosted.org/packages/39/4e/e62b01338f653059a7c0cd09d1a326e9a9eedc351a0f0de9db0601658c3d/ty-0.0.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27dd5b52a421e6871c5bfe9841160331b60866ed2040250cb161886478ab3e4f", size = 10424943, upload-time = "2026-02-05T01:07:08.777Z" }, + { url = "https://files.pythonhosted.org/packages/65/b5/7aa06655ce69c0d4f3e845d2d85e79c12994b6d84c71699cfb437e0bc8cf/ty-0.0.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76b85c9ec2219e11c358a7db8e21b7e5c6674a1fb9b6f633836949de98d12286", size = 10964692, upload-time = "2026-02-05T01:06:37.103Z" }, + { url = "https://files.pythonhosted.org/packages/13/04/36fdfe1f3c908b471e246e37ce3d011175584c26d3853e6c5d9a0364564c/ty-0.0.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9e8204c61d8ede4f21f2975dce74efdb80fafb2fae1915c666cceb33ea3c90b", size = 10692225, upload-time = "2026-02-05T01:06:49.714Z" }, + { url = "https://files.pythonhosted.org/packages/13/41/5bf882649bd8b64ded5fbce7fb8d77fb3b868de1a3b1a6c4796402b47308/ty-0.0.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af87c3be7c944bb4d6609d6c63e4594944b0028c7bd490a525a82b88fe010d6d", size = 10516776, upload-time = "2026-02-05T01:06:52.047Z" }, + { url = "https://files.pythonhosted.org/packages/56/75/66852d7e004f859839c17ffe1d16513c1e7cc04bcc810edb80ca022a9124/ty-0.0.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:50dccf7398505e5966847d366c9e4c650b8c225411c2a68c32040a63b9521eea", size = 9928828, upload-time = "2026-02-05T01:06:56.647Z" }, + { url = "https://files.pythonhosted.org/packages/65/72/96bc16c7b337a3ef358fd227b3c8ef0c77405f3bfbbfb59ee5915f0d9d71/ty-0.0.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:bd797b8f231a4f4715110259ad1ad5340a87b802307f3e06d92bfb37b858a8f3", size = 9978960, upload-time = "2026-02-05T01:06:29.567Z" }, + { url = "https://files.pythonhosted.org/packages/a0/18/d2e316a35b626de2227f832cd36d21205e4f5d96fd036a8af84c72ecec1b/ty-0.0.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9deb7f20e18b25440a9aa4884f934ba5628ef456dbde91819d5af1a73da48af3", size = 10135903, upload-time = "2026-02-05T01:06:59.256Z" }, + { url = "https://files.pythonhosted.org/packages/02/d3/b617a79c9dad10c888d7c15cd78859e0160b8772273637b9c4241a049491/ty-0.0.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7b31b3de031255b90a5f4d9cb3d050feae246067c87130e5a6861a8061c71754", size = 10615879, upload-time = "2026-02-05T01:07:06.661Z" }, + { url = "https://files.pythonhosted.org/packages/fb/b0/2652a73c71c77296a6343217063f05745da60c67b7e8a8e25f2064167fce/ty-0.0.15-py3-none-win32.whl", hash = "sha256:9362c528ceb62c89d65c216336d28d500bc9f4c10418413f63ebc16886e16cc1", size = 9578058, upload-time = "2026-02-05T01:06:42.928Z" }, + { url = "https://files.pythonhosted.org/packages/84/6e/08a4aedebd2a6ce2784b5bc3760e43d1861f1a184734a78215c2d397c1df/ty-0.0.15-py3-none-win_amd64.whl", hash = "sha256:4db040695ae67c5524f59cb8179a8fa277112e69042d7dfdac862caa7e3b0d9c", size = 10457112, upload-time = "2026-02-05T01:06:39.885Z" }, + { url = "https://files.pythonhosted.org/packages/b3/be/1991f2bc12847ae2d4f1e3ac5dcff8bb7bc1261390645c0755bb55616355/ty-0.0.15-py3-none-win_arm64.whl", hash = "sha256:e5a98d4119e77d6136461e16ae505f8f8069002874ab073de03fbcb1a5e8bf25", size = 9937490, upload-time = "2026-02-05T01:06:32.388Z" }, +] + +[[package]] +name = "typeguard" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/de/4420db493fa8fc0856d5e5c1b159c63a323d2de2317babe36b01568928e8/typeguard-4.6.0.tar.gz", hash = "sha256:e7414f09111317de3e335de92cd397c5c0ca00b1cc1676de12e1d444a79b3f21", size = 82330, upload-time = "2026-07-26T08:40:23.207Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/eb/461d5f167b6f5c7d97696f397c82f82e3480e003fce3f0a1cd1dd26e2eb2/typeguard-4.6.0-py3-none-any.whl", hash = "sha256:79878165bb86f2cf5d41d159a0ff1792a796cf496882d2fe1b1c6c7049b9cdd7", size = 36884, upload-time = "2026-07-26T08:40:21.868Z" }, +] + +[[package]] +name = "typer" +version = "0.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/78/d90f616bf5f88f8710ad067c1f8705bf7618059836ca084e5bb2a0855d75/typer-0.16.1.tar.gz", hash = "sha256:d358c65a464a7a90f338e3bb7ff0c74ac081449e53884b12ba658cbd72990614", size = 102836, upload-time = "2025-08-18T19:18:22.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/76/06dbe78f39b2203d2a47d5facc5df5102d0561e2807396471b5f7c5a30a1/typer-0.16.1-py3-none-any.whl", hash = "sha256:90ee01cb02d9b8395ae21ee3368421faf21fa138cb2a541ed369c08cec5237c9", size = 46397, upload-time = "2025-08-18T19:18:21.663Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, +] + +[[package]] +name = "wcmatch" +version = "11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bracex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/cd/0e028d8b7d906d51acdac537334fbe85f8284f2a8147d48bc9403af77a0d/wcmatch-11.1.tar.gz", hash = "sha256:f2b6f0fad6f5e2571498723fcb3c8facdd0fba0dcc139f31d32f592eb648153e", size = 129091, upload-time = "2026-10-10T13:10:54.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/22/c4adda5a3754b7fc9429a544a0673fa6922fdd7808eab09abe18616f6e38/wcmatch-11.1-py3-none-any.whl", hash = "sha256:2863afee97a7dd62e64f7feada864848c1a5e31cb331b3aa530fa1300d2d5d3c", size = 43417, upload-time = "2026-10-10T13:10:52.939Z" }, +] From 978f70fad44b46d55c44e5fd8cab3c96b2558e31 Mon Sep 17 00:00:00 2001 From: Maksim Obukhov Date: Thu, 15 Oct 2026 15:27:31 +0200 Subject: [PATCH 07/26] feat(acp-python): add connect_acp_agent connect_acp_agent(sandbox, command, client) starts the agent in an AsyncSandbox and returns an AcpAgent with the agent-client-protocol connection, close(), wait() and kill(). Async only: the ACP Python SDK is asynchronous. Messages reach receive() through a deque, so a cancelled receive never loses one. Writes, replies and EOF share one lock. close() bounds the whole shutdown from the call, kills an agent whose output stream failed without a known exit, keeps buffered messages and drops later ones, also when the reader waited for room in the queue. --- packages/acp-python/README.md | 77 ++++++ packages/acp-python/e2b_acp/__init__.py | 117 ++++++++ packages/acp-python/e2b_acp/transport.py | 258 ++++++++++++++++++ .../tests/test_transport_offline.py | 178 ++++++++++++ 4 files changed, 630 insertions(+) create mode 100644 packages/acp-python/README.md create mode 100644 packages/acp-python/e2b_acp/__init__.py create mode 100644 packages/acp-python/e2b_acp/transport.py create mode 100644 packages/acp-python/tests/test_transport_offline.py diff --git a/packages/acp-python/README.md b/packages/acp-python/README.md new file mode 100644 index 0000000000..b3484e27e4 --- /dev/null +++ b/packages/acp-python/README.md @@ -0,0 +1,77 @@ +# E2B ACP + +Run [Agent Client Protocol](https://agentclientprotocol.com) (ACP) agents such as goose, OpenCode or Claude Code inside an [E2B](https://e2b.dev) sandbox, and drive them from your code with the official ACP client. + +Source: [packages/acp-python](https://github.com/e2b-dev/E2B/tree/main/packages/acp-python) + +## Install + +``` +pip install e2b e2b-acp agent-client-protocol +``` + +Set `E2B_API_KEY` to your [E2B API key](https://e2b.dev/dashboard?tab=keys), and the provider key the agent needs. + +## Usage + +Create a sandbox from a template with the agent installed, then start the agent and connect your ACP client to it. The ACP Python SDK is asynchronous, so use `AsyncSandbox`. + +```python +import asyncio +import os + +import acp +from acp import schema +from e2b import AsyncSandbox +from e2b_acp import connect_acp_agent + + +class Client: + async def request_permission(self, session_id, tool_call, options, **kwargs): + allow = next((o for o in options if o.kind == "allow_once"), options[0]) + return schema.RequestPermissionResponse( + outcome=schema.AllowedOutcome(outcome="selected", option_id=allow.option_id) + ) + + async def session_update(self, session_id, update, **kwargs): + if isinstance(update, schema.AgentMessageChunk) and update.content.type == "text": + print(update.content.text, end="") + + +async def main(): + sandbox = await AsyncSandbox.create("goose", timeout=600) + try: + agent = await connect_acp_agent( + sandbox, + "goose acp --with-builtin developer", + Client(), + envs={ + "GOOSE_PROVIDER": "openai", + "GOOSE_MODEL": "gpt-5.6-luna", + "OPENAI_API_KEY": os.environ["OPENAI_API_KEY"], + }, + ) + await agent.connection.initialize(protocol_version=acp.PROTOCOL_VERSION) + session = await agent.connection.new_session(cwd="/home/user", mcp_servers=[]) + await agent.connection.prompt( + session_id=session.session_id, + prompt=[acp.text_block("Create fizzbuzz.py for 1 to 15 and run it.")], + ) + await agent.close() + finally: + await sandbox.kill() + + +asyncio.run(main()) +``` + +## Where the agent's tools run + +The agent talks ACP over its stdin and stdout. `connect_acp_agent` turns off the client filesystem and terminal capabilities in `initialize` and answers agent requests for `fs/*` or `terminal/*` with an error, so the agent edits files and runs commands with its own tools inside the sandbox. It does not restrict the agent's network access or the MCP servers you pass to it. + +## Lifecycle + +- `agent.close()` sends EOF to the agent and kills it if it has not exited within `timeout` (5 seconds by default) of the call. Messages the agent sends after that are not delivered. +- `agent.wait()` returns the exit code once the agent exits. +- If the agent exits with an error, the connection fails with the exit code and the end of the agent's stderr. Pass `on_stderr` to receive its logs. +- The agent runs as long as the sandbox does, so set the sandbox timeout for the longest task you expect. diff --git a/packages/acp-python/e2b_acp/__init__.py b/packages/acp-python/e2b_acp/__init__.py new file mode 100644 index 0000000000..1994672dc9 --- /dev/null +++ b/packages/acp-python/e2b_acp/__init__.py @@ -0,0 +1,117 @@ +""" +Run [Agent Client Protocol](https://agentclientprotocol.com) agents in E2B sandboxes. +""" + +from typing import Dict, Optional + +import acp +from e2b import AsyncSandbox +from e2b.connection_config import Username + +from .transport import AcpAgentExit, AcpTransport, StderrHandler +from .utils import DEFAULT_MAX_MESSAGE_BYTES + + +class AcpAgent: + """ + ACP agent running in a sandbox, connected to your client. + """ + + def __init__(self, transport: AcpTransport, connection: acp.ClientSideConnection): + self._transport = transport + self._connection = connection + + @property + def connection(self) -> acp.ClientSideConnection: + """ + Connection to the agent. Use it to initialize, create sessions and send prompts. + """ + return self._connection + + @property + def pid(self) -> int: + """ + Process ID of the agent in the sandbox. + """ + return self._transport.pid + + async def wait(self) -> AcpAgentExit: + """ + Wait for the agent process to exit. + + It raises if the agent's output stream failed before its exit status was known. + + :return: Exit status of the agent + """ + return await self._transport.wait() + + async def close(self, timeout: float = 5) -> None: + """ + Close the agent: send EOF to its stdin and kill it if it has not + exited within `timeout` of this call. Messages the agent sends after + this call are not delivered. + + :param timeout: Time for the agent to exit in **seconds** + """ + await self._transport.close(timeout) + + async def kill(self) -> bool: + """ + Kill the agent process with `SIGKILL`. + + :return: `True` if the agent was killed, `False` if it was not found + """ + return await self._transport.kill() + + +async def connect_acp_agent( + sandbox: AsyncSandbox, + command: str, + client: acp.Client, + cwd: Optional[str] = None, + envs: Optional[Dict[str, str]] = None, + user: Optional[Username] = None, + on_stderr: Optional[StderrHandler] = None, + max_message_bytes: int = DEFAULT_MAX_MESSAGE_BYTES, + request_timeout: Optional[float] = None, +) -> AcpAgent: + """ + Start an ACP agent in the sandbox and connect your ACP client to it over + the agent's stdin/stdout. + + The client filesystem and terminal capabilities are turned off in + `initialize`, and agent requests for `fs/*` or `terminal/*` are answered + with an error without reaching your client, so the agent edits files and + runs commands with its own tools in the sandbox. Network access and MCP + servers you pass to the agent are not restricted. + + ```python + import acp + from e2b import AsyncSandbox + from e2b_acp import connect_acp_agent + + sandbox = await AsyncSandbox.create("goose") + agent = await connect_acp_agent(sandbox, "goose acp", client) + await agent.connection.initialize(protocol_version=acp.PROTOCOL_VERSION) + # ... + await agent.close() + ``` + + :param sandbox: Sandbox with the agent installed + :param command: Command that starts the agent in ACP mode, for example `goose acp` + :param client: Your ACP client. It answers permission requests and receives the agent's session updates + :param cwd: Working directory of the agent + :param envs: Environment variables for the agent + :param user: User to run the agent as + :param on_stderr: Callback for the agent's stderr output (logs). The agent's stdout is reserved for the protocol + :param max_message_bytes: Maximum size of a single message from the agent in **bytes** + :param request_timeout: Timeout for starting the agent in **seconds** + + :return: `AcpAgent` with the ACP connection + """ + transport = AcpTransport(sandbox, max_message_bytes, on_stderr) + await transport.start(command, envs, user, cwd, request_timeout) + return AcpAgent(transport, acp.connect_to_agent(client, transport)) + + +__all__ = ["AcpAgent", "AcpAgentExit", "connect_acp_agent"] diff --git a/packages/acp-python/e2b_acp/transport.py b/packages/acp-python/e2b_acp/transport.py new file mode 100644 index 0000000000..753e426ebb --- /dev/null +++ b/packages/acp-python/e2b_acp/transport.py @@ -0,0 +1,258 @@ +import asyncio +import inspect +from collections import deque +from dataclasses import dataclass +from typing import Any, Awaitable, Callable, Deque, Dict, Optional, Union + +from e2b import AsyncCommandHandle, AsyncSandbox, CommandExitException, SandboxException +from e2b.connection_config import Username +from e2b.envd.versions import ENVD_ENVD_CLOSE + +from .utils import ( + JsonRpcMessage, + LineSplitter, + encode_message, + parse_agent_line, + restrict_client_capabilities, +) + +STDERR_TAIL_CHARS = 4096 +READ_QUEUE_MESSAGES = 64 + +StderrHandler = Union[Callable[[str], None], Callable[[str], Awaitable[None]]] + + +@dataclass +class AcpAgentExit: + """ + Exit status of an ACP agent process. + """ + + exit_code: int + """ + Exit code of the agent process. + """ + error: Optional[str] = None + """ + Error message if the process failed. + """ + + +class AcpTransport: + """ + ACP over the stdin/stdout of an agent process in the sandbox. + + It implements the transport interface of the `agent-client-protocol` + package (`send`, `receive`, `close`). + """ + + def __init__( + self, + sandbox: AsyncSandbox, + max_message_bytes: int, + on_stderr: Optional[StderrHandler], + ): + self._commands = sandbox.commands + self._lines = LineSplitter(max_message_bytes) + self._on_stderr = on_stderr + self._stderr_tail = "" + + # Messages for the client. receive() and the reader hand them over + # through the deque, so a cancelled receive() never loses one. + self._pending: Deque[Any] = deque() + self._readable = asyncio.Event() + self._space = asyncio.Event() + self._space.set() + + self._write_lock = asyncio.Lock() + self._handle_ready: asyncio.Future[AsyncCommandHandle] = ( + asyncio.get_running_loop().create_future() + ) + self._handle: Optional[AsyncCommandHandle] = None + self._exit_task: Optional[asyncio.Task[Optional[AcpAgentExit]]] = None + + self._ended = False + self._error: Optional[BaseException] = None + # Set only once the process exit status is known. + self._exit: Optional[AcpAgentExit] = None + # Set by close(): the exit that follows is expected, and output after + # it is not delivered. + self._closing = False + + @property + def pid(self) -> int: + assert self._handle is not None + return self._handle.pid + + async def send(self, message: JsonRpcMessage) -> None: + await self._write(restrict_client_capabilities(message)) + + async def receive(self) -> Optional[JsonRpcMessage]: + while True: + if self._pending: + message = self._pending.popleft() + if len(self._pending) < READ_QUEUE_MESSAGES: + self._space.set() + return message + if self._ended: + if self._error is not None: + raise self._error + return None + self._readable.clear() + await self._readable.wait() + + async def close(self, timeout: float = 5) -> None: + # Only a known exit status means the process is gone. A failed output + # stream can leave it running. + if self._exit is not None or self._exit_task is None: + return + + self._closing = True + # Unblock the reader if it waits for the client. + self._space.set() + + try: + exit = await asyncio.wait_for(self._stop_and_wait(), timeout) + except Exception: + exit = None + if exit is None and self._exit is None: + try: + await self.kill() + except Exception: + pass + + async def kill(self) -> bool: + assert self._handle is not None + return await self._handle.kill() + + async def wait(self) -> AcpAgentExit: + assert self._exit_task is not None + exit = await asyncio.shield(self._exit_task) + if exit is None: + assert self._error is not None + raise self._error + return exit + + async def start( + self, + cmd: str, + envs: Optional[Dict[str, str]], + user: Optional[Username], + cwd: Optional[str], + request_timeout: Optional[float], + ) -> None: + try: + handle = await self._commands._start( + cmd, + envs=envs, + user=user, + cwd=cwd, + stdin=True, + timeout=0, + request_timeout=request_timeout, + on_stdout=self._handle_stdout, + on_stderr=self._handle_stderr, + retain_output=False, + ) + except BaseException: + self._handle_ready.cancel() + raise + self._handle = handle + self._handle_ready.set_result(handle) + self._exit_task = asyncio.create_task(self._watch_exit(handle)) + + async def _stop_and_wait(self) -> Optional[AcpAgentExit]: + await self._end_input() + assert self._exit_task is not None + return await asyncio.shield(self._exit_task) + + async def _write(self, message: Any) -> None: + line = encode_message(message) + handle = await asyncio.shield(self._handle_ready) + # Client messages, replies to the agent and EOF share one ordered + # write path, so concurrent writes never interleave. + async with self._write_lock: + await handle.send_stdin(line) + + async def _end_input(self) -> None: + handle = await asyncio.shield(self._handle_ready) + async with self._write_lock: + if self._commands._envd_version >= ENVD_ENVD_CLOSE: + await handle.close_stdin() + else: + await handle.kill() + + def _end(self, error: Optional[BaseException] = None) -> None: + if not self._ended: + self._ended = True + self._error = error + self._readable.set() + self._space.set() + + async def _deliver(self, line: str) -> None: + if self._closing: + return + message, replies = parse_agent_line(line) + for reply in replies: + try: + await self._write(reply) + except Exception: + pass + if message is None: + return + # Stop reading agent output until the client catches up. + while len(self._pending) >= READ_QUEUE_MESSAGES and not ( + self._closing or self._ended + ): + self._space.clear() + await self._space.wait() + if self._closing or self._ended: + return + self._pending.append(message) + self._readable.set() + + async def _handle_stdout(self, data: str) -> None: + if self._closing or self._ended: + return + try: + lines = self._lines.push(data) + except SandboxException as e: + self._end(e) + asyncio.ensure_future(self.close()) + return + for line in lines: + await self._deliver(line) + + async def _handle_stderr(self, data: str) -> None: + self._stderr_tail = (self._stderr_tail + data)[-STDERR_TAIL_CHARS:] + if self._on_stderr: + result = self._on_stderr(data) + if inspect.isawaitable(result): + await result + + async def _watch_exit(self, handle: AsyncCommandHandle) -> Optional[AcpAgentExit]: + try: + result = await handle.wait() + exit = AcpAgentExit(exit_code=result.exit_code, error=result.error) + except CommandExitException as e: + exit = AcpAgentExit(exit_code=e.exit_code, error=e.error) + except Exception as e: + # The output stream failed; the process may still be running. + self._end(e) + return None + + self._exit = exit + try: + await self._deliver(self._lines.flush()) + except SandboxException as e: + self._end(e) + if exit.exit_code != 0 and not self._closing: + message = f"ACP agent exited with code {exit.exit_code}" + if exit.error: + message += f": {exit.error}" + if self._stderr_tail: + message += f"\n{self._stderr_tail}" + self._end(SandboxException(message)) + else: + self._end() + return exit diff --git a/packages/acp-python/tests/test_transport_offline.py b/packages/acp-python/tests/test_transport_offline.py new file mode 100644 index 0000000000..6dc768810f --- /dev/null +++ b/packages/acp-python/tests/test_transport_offline.py @@ -0,0 +1,178 @@ +import asyncio +import json +import time +from types import SimpleNamespace + +from packaging.version import Version + +from e2b import CommandExitException +from e2b_acp.transport import READ_QUEUE_MESSAGES, AcpTransport + + +class FakeAgent: + """A command handle driven by the test instead of a sandbox.""" + + def __init__(self, close_stdin=None): + self.writes = [] + self.active = 0 + self.max_active = 0 + self.killed = 0 + self.exit = asyncio.get_running_loop().create_future() + self._close_stdin = close_stdin + self.handle = SimpleNamespace( + pid=42, + send_stdin=self._send_stdin, + close_stdin=self._close, + kill=self._kill, + wait=self._wait, + ) + self.sandbox = SimpleNamespace( + commands=SimpleNamespace(_envd_version=Version("0.9.0"), _start=self._start) + ) + + async def _start(self, cmd, on_stdout, **kwargs): + self.on_stdout = on_stdout + return self.handle + + async def _send_stdin(self, data): + self.active += 1 + self.max_active = max(self.max_active, self.active) + await asyncio.sleep(0.002) + self.writes.append(data) + self.active -= 1 + + async def _close(self): + if self._close_stdin: + await self._close_stdin() + + async def _kill(self): + self.killed += 1 + self.exit_with(-1) + return True + + async def _wait(self): + return await asyncio.shield(self.exit) + + def exit_with(self, code): + if self.exit.done(): + return + if code == 0: + self.exit.set_result(SimpleNamespace(exit_code=0, error=None)) + else: + self.exit.set_exception( + CommandExitException(stdout="", stderr="", exit_code=code, error=None) + ) + + def emit(self, message): + return self.on_stdout(json.dumps(message) + "\n") + + +async def start(agent): + transport = AcpTransport(agent.sandbox, 1024, None) + await transport.start("agent", None, None, None, None) + return transport + + +async def hang(): + await asyncio.Event().wait() + + +async def test_writes_and_replies_never_overlap(): + agent = FakeAgent() + transport = await start(agent) + + await asyncio.gather( + *( + transport.send({"jsonrpc": "2.0", "id": i, "method": "echo"}) + for i in range(10) + ), + *( + agent.emit( + {"jsonrpc": "2.0", "id": f"fs-{i}", "method": "fs/read_text_file"} + ) + for i in range(10) + ), + ) + + assert len(agent.writes) == 20 + assert agent.max_active == 1 + + +async def test_cancelled_receive_does_not_lose_a_message(): + agent = FakeAgent() + transport = await start(agent) + + waiting = asyncio.create_task(transport.receive()) + await asyncio.sleep(0) + await agent.emit({"jsonrpc": "2.0", "method": "update"}) + # Let the waiting receive() wake up, then cancel it. + await asyncio.sleep(0) + waiting.cancel() + await asyncio.gather(waiting, return_exceptions=True) + + if waiting.cancelled(): + # The message stayed queued for the next receive(). + message = await asyncio.wait_for(transport.receive(), 1) + else: + message = waiting.result() + assert message["method"] == "update" + + +async def test_close_kills_the_agent_when_its_output_stream_failed(): + agent = FakeAgent() + transport = await start(agent) + + agent.exit.set_exception(RuntimeError("output stream broke")) + await asyncio.sleep(0) + await transport.close(timeout=0.05) + + assert agent.killed == 1 + + +async def test_close_timeout_covers_a_hung_eof_and_then_kills(): + agent = FakeAgent(close_stdin=hang) + transport = await start(agent) + + started = time.monotonic() + await transport.close(timeout=0.05) + + assert time.monotonic() - started < 1 + assert agent.killed == 1 + + +async def test_close_keeps_buffered_messages_and_drops_later_ones(): + agent = FakeAgent() + transport = await start(agent) + + await agent.emit({"jsonrpc": "2.0", "method": "before"}) + closing = asyncio.create_task(transport.close(timeout=0.05)) + await asyncio.sleep(0) + await agent.emit({"jsonrpc": "2.0", "method": "after"}) + agent.exit_with(0) + await closing + + assert (await transport.receive())["method"] == "before" + assert await transport.receive() is None + + +async def test_close_releases_a_reader_blocked_on_a_full_queue(): + agent = FakeAgent() + transport = await start(agent) + for i in range(READ_QUEUE_MESSAGES): + await agent.emit({"jsonrpc": "2.0", "method": "update", "params": i}) + + # The next message waits for room in the queue. + blocked = asyncio.create_task(agent.emit({"jsonrpc": "2.0", "method": "late"})) + await asyncio.sleep(0.01) + assert not blocked.done() + + closing = asyncio.create_task(transport.close(timeout=0.05)) + await asyncio.wait_for(blocked, 1) + agent.exit_with(0) + await closing + + received = [] + while (message := await transport.receive()) is not None: + received.append(message["method"]) + assert "late" not in received + assert len(received) == READ_QUEUE_MESSAGES From 106f7688eafe42516c7b8be17415c64e95007cc2 Mon Sep 17 00:00:00 2001 From: Maksim Obukhov Date: Thu, 15 Oct 2026 15:27:31 +0200 Subject: [PATCH 08/26] test(acp-python): cover connect_acp_agent against a scripted agent --- packages/acp-python/tests/acp_fake_agent.py | 72 ++++++++ packages/acp-python/tests/conftest.py | 12 ++ packages/acp-python/tests/test_connect.py | 30 ++++ packages/acp-python/tests/test_transport.py | 189 ++++++++++++++++++++ 4 files changed, 303 insertions(+) create mode 100644 packages/acp-python/tests/acp_fake_agent.py create mode 100644 packages/acp-python/tests/conftest.py create mode 100644 packages/acp-python/tests/test_connect.py create mode 100644 packages/acp-python/tests/test_transport.py diff --git a/packages/acp-python/tests/acp_fake_agent.py b/packages/acp-python/tests/acp_fake_agent.py new file mode 100644 index 0000000000..55bc01d9de --- /dev/null +++ b/packages/acp-python/tests/acp_fake_agent.py @@ -0,0 +1,72 @@ +"""Minimal deterministic ACP-shaped agent driven by test methods. + +Kept in sync with packages/acp-js/tests/fakeAgent.ts. +""" + +FAKE_AGENT = r""" +import json, os, sys, time + +def send(message): + sys.stdout.write(json.dumps(message, ensure_ascii=False) + "\n") + sys.stdout.flush() + +pending = {} +errors = [] +for line in sys.stdin: + message = json.loads(line) + method, id, params = message.get("method"), message.get("id"), message.get("params") + if method is None and message.get("id") is None: + # Error response without an id, such as a parse error. + errors.append(message.get("error")) + continue + if method is None: + # Response to one of our requests. + origin = pending.pop(message["id"], None) + if origin is not None: + send({"jsonrpc": "2.0", "id": origin, "result": message}) + continue + if method == "initialize": + send({"jsonrpc": "2.0", "id": id, "result": {"protocolVersion": 1, "received": params}}) + elif method == "test/echo": + send({"jsonrpc": "2.0", "id": id, "result": params}) + elif method == "test/client_call": + # Ask the client to run something on its machine. + pending["call-" + str(id)] = id + send({"jsonrpc": "2.0", "id": "call-" + str(id), "method": params["method"], "params": {}}) + elif method == "test/notify_client": + send({"jsonrpc": "2.0", "method": "terminal/output", "params": {}}) + send({"jsonrpc": "2.0", "id": id, "result": "sent"}) + elif method == "test/bytewise": + data = (json.dumps({"jsonrpc": "2.0", "id": id, "result": params["text"]}, ensure_ascii=False) + "\n").encode() + for b in data: + os.write(1, bytes([b])) + time.sleep(0.002) + elif method == "test/garbage": + sys.stdout.write("not json\n") + sys.stdout.flush() + send({"jsonrpc": "2.0", "id": id, "result": "garbage sent"}) + elif method == "test/errors": + send({"jsonrpc": "2.0", "id": id, "result": errors}) + elif method == "test/big": + send({"jsonrpc": "2.0", "id": id, "result": "x" * params["size"]}) + elif method == "test/stderr_then_sleep": + sys.stderr.write("log line\n") + sys.stderr.flush() + send({"jsonrpc": "2.0", "id": id, "result": "sleeping"}) + time.sleep(600) + elif method == "test/exit": + sys.stderr.write(params.get("stderr", "")) + sys.stderr.flush() + sys.exit(params["code"]) + elif method == "test/flood": + for i in range(params["count"]): + send({"jsonrpc": "2.0", "method": "session/update", "params": {"i": i}}) + send({"jsonrpc": "2.0", "id": id, "result": "flooded"}) + elif method == "test/ignore_eof": + os.close(0) + send({"jsonrpc": "2.0", "id": id, "result": "ignoring"}) + time.sleep(600) +""" + +FAKE_AGENT_PATH = "/tmp/fake_acp_agent.py" +FAKE_AGENT_CMD = f"python3 -u {FAKE_AGENT_PATH}" diff --git a/packages/acp-python/tests/conftest.py b/packages/acp-python/tests/conftest.py new file mode 100644 index 0000000000..413cbb6a8c --- /dev/null +++ b/packages/acp-python/tests/conftest.py @@ -0,0 +1,12 @@ +import pytest_asyncio + +from e2b import AsyncSandbox + + +@pytest_asyncio.fixture +async def async_sandbox(): + sandbox = await AsyncSandbox.create(timeout=300) + try: + yield sandbox + finally: + await sandbox.kill() diff --git a/packages/acp-python/tests/test_connect.py b/packages/acp-python/tests/test_connect.py new file mode 100644 index 0000000000..1428dfb0c0 --- /dev/null +++ b/packages/acp-python/tests/test_connect.py @@ -0,0 +1,30 @@ +import acp +from acp import schema + +from e2b import AsyncSandbox +from e2b_acp import connect_acp_agent +from acp_fake_agent import FAKE_AGENT, FAKE_AGENT_CMD, FAKE_AGENT_PATH + + +class Client: + async def request_permission(self, session_id, tool_call, options, **kwargs): + return schema.RequestPermissionResponse( + outcome=schema.DeniedOutcome(outcome="cancelled") + ) + + async def session_update(self, session_id, update, **kwargs): + pass + + +async def test_connect_acp_agent_connects_an_acp_client(async_sandbox: AsyncSandbox): + await async_sandbox.files.write(FAKE_AGENT_PATH, FAKE_AGENT) + agent = await connect_acp_agent(async_sandbox, FAKE_AGENT_CMD, Client()) + + response = await agent.connection.initialize( + protocol_version=acp.PROTOCOL_VERSION, + client_capabilities=schema.ClientCapabilities(terminal=True), + ) + + assert response.protocol_version == 1 + await agent.close() + assert (await agent.wait()).exit_code == 0 diff --git a/packages/acp-python/tests/test_transport.py b/packages/acp-python/tests/test_transport.py new file mode 100644 index 0000000000..a62bb6247a --- /dev/null +++ b/packages/acp-python/tests/test_transport.py @@ -0,0 +1,189 @@ +import asyncio +import time + +import pytest + +from e2b import AsyncSandbox +from e2b_acp.transport import AcpTransport +from e2b_acp.utils import DEFAULT_MAX_MESSAGE_BYTES +from acp_fake_agent import FAKE_AGENT, FAKE_AGENT_CMD, FAKE_AGENT_PATH + + +async def start_agent( + sandbox: AsyncSandbox, + max_message_bytes=DEFAULT_MAX_MESSAGE_BYTES, + on_stderr=None, +): + await sandbox.files.write(FAKE_AGENT_PATH, FAKE_AGENT) + agent = AcpTransport(sandbox, max_message_bytes, on_stderr) + await agent.start(FAKE_AGENT_CMD, None, None, None, None) + + async def call(id, method, params=None): + await agent.send( + {"jsonrpc": "2.0", "id": id, "method": method, "params": params or {}} + ) + return await agent.receive() + + return agent, call + + +async def test_initialize_reaches_agent_without_client_fs_and_terminal( + async_sandbox: AsyncSandbox, +): + agent, call = await start_agent(async_sandbox) + + response = await call( + 0, + "initialize", + { + "protocolVersion": 1, + "clientCapabilities": { + "fs": {"readTextFile": True, "writeTextFile": True}, + "terminal": True, + }, + }, + ) + + assert response["result"]["received"]["clientCapabilities"] == { + "fs": {"readTextFile": False, "writeTextFile": False}, + "terminal": False, + } + await agent.close() + + +async def test_agent_requests_for_client_fs_and_terminal_are_rejected( + async_sandbox: AsyncSandbox, +): + agent, call = await start_agent(async_sandbox) + + for id, method in [ + (1, "fs/write_text_file"), + (2, "fs/read_text_file"), + (3, "terminal/create"), + ]: + # The client only sees the agent's final answer, never the request. + response = await call(id, "test/client_call", {"method": method}) + assert response["id"] == id + assert response["result"]["error"]["code"] == -32601 + + # Notifications are dropped; the next message is the response. + assert (await call(4, "test/notify_client"))["result"] == "sent" + await agent.close() + + +async def test_writes_keep_their_order(async_sandbox: AsyncSandbox): + agent, _ = await start_agent(async_sandbox) + count = 100 + + await asyncio.gather( + *( + agent.send({"jsonrpc": "2.0", "id": i, "method": "test/echo", "params": i}) + for i in range(count) + ) + ) + + for i in range(count): + assert (await agent.receive())["result"] == i + await agent.close() + + +async def test_messages_split_into_single_bytes_are_reassembled( + async_sandbox: AsyncSandbox, +): + agent, call = await start_agent(async_sandbox) + text = "Привет 🌍 E2B" + + assert (await call(5, "test/bytewise", {"text": text}))["result"] == text + await agent.close() + + +async def test_invalid_agent_output_gets_parse_error_and_stream_continues( + async_sandbox: AsyncSandbox, +): + agent, call = await start_agent(async_sandbox) + + assert (await call(6, "test/garbage"))["result"] == "garbage sent" + + # The agent got a parse error reply, and the stream still works. + assert (await call(7, "test/errors"))["result"][0]["code"] == -32700 + await agent.close() + + +async def test_messages_over_the_limit_fail_the_stream(async_sandbox: AsyncSandbox): + agent, call = await start_agent(async_sandbox, max_message_bytes=1024) + + with pytest.raises(Exception, match="exceeds the limit of 1024 bytes"): + await call(7, "test/big", {"size": 4096}) + await agent.close() + + +async def test_close_sends_eof_and_ends_the_stream(async_sandbox: AsyncSandbox): + agent, _ = await start_agent(async_sandbox) + + await agent.close() + + assert await agent.receive() is None + exit = await agent.wait() + assert exit.exit_code == 0 + + +async def test_close_kills_an_agent_that_ignores_eof(async_sandbox: AsyncSandbox): + agent, call = await start_agent(async_sandbox) + assert (await call(8, "test/ignore_eof"))["result"] == "ignoring" + + started = time.monotonic() + await agent.close(timeout=1) + + assert time.monotonic() - started < 10 + assert await agent.receive() is None + processes = await async_sandbox.commands.list() + assert all(p.pid != agent.pid for p in processes) + + +async def test_agent_failure_errors_the_stream_with_its_stderr( + async_sandbox: AsyncSandbox, +): + stderr = [] + agent, call = await start_agent(async_sandbox, on_stderr=stderr.append) + + with pytest.raises(Exception, match=r"exited with code 3[\s\S]*boom"): + await call(9, "test/exit", {"code": 3, "stderr": "boom"}) + assert "".join(stderr) == "boom" + assert (await agent.wait()).exit_code == 3 + + +async def test_close_does_not_hang_when_the_client_stops_reading( + async_sandbox: AsyncSandbox, +): + agent, _ = await start_agent(async_sandbox) + + # Far more messages than the read queue holds, none of them read. + await agent.send( + {"jsonrpc": "2.0", "id": 10, "method": "test/flood", "params": {"count": 1000}} + ) + await asyncio.sleep(2) + + await agent.close(timeout=2) + + assert (await agent.wait()).exit_code == 0 + + +async def test_close_stops_the_agent_when_the_stderr_callback_failed( + async_sandbox: AsyncSandbox, +): + def fail(data): + raise RuntimeError("callback failed") + + agent, _ = await start_agent(async_sandbox, on_stderr=fail) + await agent.send( + {"jsonrpc": "2.0", "id": 11, "method": "test/stderr_then_sleep", "params": {}} + ) + + # The failed callback ends the output stream, but not the process. + with pytest.raises(Exception, match="callback failed"): + await agent.receive() + assert any(p.pid == agent.pid for p in await async_sandbox.commands.list()) + + await agent.close(timeout=1) + + assert all(p.pid != agent.pid for p in await async_sandbox.commands.list()) From 3dd3d3d5329c9a35f6062a62f6c2bcc28684101e Mon Sep 17 00:00:00 2001 From: Maksim Obukhov Date: Thu, 15 Oct 2026 15:28:43 +0200 Subject: [PATCH 09/26] ci: test and release the ACP packages Run the @e2b/acp and e2b-acp suites when their packages or the base SDKs change, and include them in the release preflight, tests and publish. --- .github/workflows/acp_js_tests.yml | 68 ++++++++++++++++++++++++++ .github/workflows/acp_python_tests.yml | 53 ++++++++++++++++++++ .github/workflows/release.yml | 34 +++++++++++++ .github/workflows/sdk_tests.yml | 35 +++++++++++++ 4 files changed, 190 insertions(+) create mode 100644 .github/workflows/acp_js_tests.yml create mode 100644 .github/workflows/acp_python_tests.yml diff --git a/.github/workflows/acp_js_tests.yml b/.github/workflows/acp_js_tests.yml new file mode 100644 index 0000000000..8da4b6311f --- /dev/null +++ b/.github/workflows/acp_js_tests.yml @@ -0,0 +1,68 @@ +name: Test ACP JS SDK + +on: + workflow_call: + inputs: + E2B_DOMAIN: + required: false + type: string + default: '' + secrets: + E2B_API_KEY: + required: true + +permissions: + contents: read + +jobs: + test: + defaults: + run: + working-directory: ./packages/acp-js + shell: bash + name: ACP JS SDK - Build and test + runs-on: ubuntu-22.04 + steps: + - name: Checkout repository + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + + - name: Parse .tool-versions + uses: wistia/parse-tool-versions@32f568a4ffd4bfa7720ebf93f171597d1ebc979a # v2.1.1 + with: + filename: '.tool-versions' + uppercase: 'true' + prefix: 'tool_version_' + + - name: Install pnpm + uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + with: + version: '${{ env.TOOL_VERSION_PNPM }}' + + - name: Setup Node + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 + with: + node-version: '${{ env.TOOL_VERSION_NODEJS }}' + registry-url: 'https://registry.npmjs.org' + cache: pnpm + cache-dependency-path: pnpm-lock.yaml + + - name: Configure pnpm + run: | + pnpm config set auto-install-peers true + pnpm config set exclude-links-from-lockfile true + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build base JS SDK + run: pnpm --dir ../js-sdk build + + - name: Test build + run: pnpm build + + - name: Run tests + run: pnpm test + env: + E2B_API_KEY: ${{ secrets.E2B_API_KEY }} + E2B_DOMAIN: ${{ inputs.E2B_DOMAIN }} + E2B_TEST_MAX_WORKERS: 1 diff --git a/.github/workflows/acp_python_tests.yml b/.github/workflows/acp_python_tests.yml new file mode 100644 index 0000000000..d7f6eb71c1 --- /dev/null +++ b/.github/workflows/acp_python_tests.yml @@ -0,0 +1,53 @@ +name: Test ACP Python SDK + +on: + workflow_call: + inputs: + E2B_DOMAIN: + required: false + type: string + default: '' + secrets: + E2B_API_KEY: + required: true + +permissions: + contents: read + +jobs: + test: + defaults: + run: + working-directory: ./packages/acp-python + shell: bash + name: ACP Python SDK - Build and test + runs-on: ubuntu-22.04 + steps: + - name: Checkout repository + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + + - name: Parse .tool-versions + uses: wistia/parse-tool-versions@32f568a4ffd4bfa7720ebf93f171597d1ebc979a # v2.1.1 + with: + filename: '.tool-versions' + uppercase: 'true' + prefix: 'tool_version_' + + - name: Install uv + uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0 + with: + version: '${{ env.TOOL_VERSION_UV }}' + python-version: '${{ env.TOOL_VERSION_PYTHON }}' + enable-cache: true + + - name: Install dependencies + run: uv sync --locked --package e2b-acp + + - name: Test build + run: uv build + + - name: Run tests + run: uv run pytest --verbose + env: + E2B_API_KEY: ${{ secrets.E2B_API_KEY }} + E2B_DOMAIN: ${{ inputs.E2B_DOMAIN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3fef4a5d34..59c260df5d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,6 +24,8 @@ jobs: desktop_python: ${{ steps.desktop_python.outputs.release }} dockerfile_utils_js: ${{ steps.dockerfile_utils_js.outputs.release }} dockerfile_utils_python: ${{ steps.dockerfile_utils_python.outputs.release }} + acp_js: ${{ steps.acp_js.outputs.release }} + acp_python: ${{ steps.acp_python.outputs.release }} itinerary: ${{ steps.itinerary.outputs.itinerary }} steps: - name: Check the ref @@ -138,6 +140,20 @@ jobs: IS_RELEASE=$(./.github/scripts/is_release_for_package.sh "@e2b/dockerfile-utils-python") echo "release=$IS_RELEASE" >> "$GITHUB_OUTPUT" + - name: Check ACP JavaScript SDK Release + id: acp_js + if: steps.version.outputs.release == 'true' + run: | + IS_RELEASE=$(./.github/scripts/is_release_for_package.sh "@e2b/acp") + echo "release=$IS_RELEASE" >> "$GITHUB_OUTPUT" + + - name: Check ACP Python SDK Release + id: acp_python + if: steps.version.outputs.release == 'true' + run: | + IS_RELEASE=$(./.github/scripts/is_release_for_package.sh "@e2b/acp-python") + echo "release=$IS_RELEASE" >> "$GITHUB_OUTPUT" + - name: Build release itinerary id: itinerary if: steps.version.outputs.release == 'true' @@ -215,6 +231,20 @@ jobs: if: needs.preflight.outputs.dockerfile_utils_python == 'true' uses: ./.github/workflows/dockerfile_utils_python_tests.yml + acp-js-tests: + name: ACP JS SDK Tests + needs: [preflight] + if: needs.preflight.outputs.acp_js == 'true' + uses: ./.github/workflows/acp_js_tests.yml + secrets: inherit + + acp-python-tests: + name: ACP Python SDK Tests + needs: [preflight] + if: needs.preflight.outputs.acp_python == 'true' + uses: ./.github/workflows/acp_python_tests.yml + secrets: inherit + publish: name: Publish needs: @@ -228,6 +258,8 @@ jobs: - desktop-python-tests - dockerfile-utils-js-tests - dockerfile-utils-python-tests + - acp-js-tests + - acp-python-tests if: (!cancelled()) && !contains(needs.*.result, 'failure') && needs.preflight.outputs.release == 'true' uses: ./.github/workflows/publish_packages.yml secrets: inherit @@ -265,6 +297,8 @@ jobs: - desktop-python-tests - dockerfile-utils-js-tests - dockerfile-utils-python-tests + - acp-js-tests + - acp-python-tests - publish if: failure() name: Release Failed - Slack Notification diff --git a/.github/workflows/sdk_tests.yml b/.github/workflows/sdk_tests.yml index 4aeaea69a1..3404e70c23 100644 --- a/.github/workflows/sdk_tests.yml +++ b/.github/workflows/sdk_tests.yml @@ -35,6 +35,8 @@ jobs: desktop_python: ${{ steps.filter.outputs.desktop_python }} dockerfile_utils_js: ${{ steps.filter.outputs.dockerfile_utils_js }} dockerfile_utils_python: ${{ steps.filter.outputs.dockerfile_utils_python }} + acp_js: ${{ steps.filter.outputs.acp_js }} + acp_python: ${{ steps.filter.outputs.acp_python }} steps: - name: Filter changed paths # On workflow_dispatch there is no PR diff and paths-filter would fall @@ -71,6 +73,21 @@ jobs: - 'packages/cli/**/!(*.md)' - 'packages/js-sdk/**/!(*.md)' - 'packages/dockerfile-utils-js/**/!(*.md)' + acp_js: + - *shared + - '.github/workflows/acp_js_tests.yml' + - 'tsconfig.sdk.json' + - 'tsdown.sdk.config.mts' + - 'vitest.sdk.config.mts' + - 'packages/acp-js/**/!(*.md)' + - 'packages/js-sdk/**/!(*.md)' + - 'packages/dockerfile-utils-js/**/!(*.md)' + acp_python: + - *shared + - '.github/workflows/acp_python_tests.yml' + - 'packages/acp-python/**/!(*.md)' + - 'packages/python-sdk/**/!(*.md)' + - 'packages/dockerfile-utils-python/**/!(*.md)' code_interpreter_js: - *shared - '.github/workflows/code_interpreter_js_tests.yml' @@ -136,6 +153,22 @@ jobs: secrets: E2B_API_KEY: ${{ secrets.E2B_API_KEY }} + acp-js-tests: + name: Production / ACP JS SDK Tests + needs: changes + if: ${{ needs.changes.outputs.acp_js == 'true' || github.event_name == 'workflow_dispatch' }} + uses: ./.github/workflows/acp_js_tests.yml + secrets: + E2B_API_KEY: ${{ secrets.E2B_API_KEY }} + + acp-python-tests: + name: Production / ACP Python SDK Tests + needs: changes + if: ${{ needs.changes.outputs.acp_python == 'true' || github.event_name == 'workflow_dispatch' }} + uses: ./.github/workflows/acp_python_tests.yml + secrets: + E2B_API_KEY: ${{ secrets.E2B_API_KEY }} + code-interpreter-js-tests: name: Production / Code Interpreter JS SDK Tests needs: changes @@ -269,6 +302,8 @@ jobs: - js-tests - python-tests - cli-tests + - acp-js-tests + - acp-python-tests - code-interpreter-js-tests - code-interpreter-python-tests - desktop-js-tests From 5e904b27e80ddf10f96f0d6dafac098bb9aeae62 Mon Sep 17 00:00:00 2001 From: Maksim Obukhov Date: Thu, 15 Oct 2026 15:28:43 +0200 Subject: [PATCH 10/26] chore: add changeset for the ACP packages --- .changeset/acp-package.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/acp-package.md diff --git a/.changeset/acp-package.md b/.changeset/acp-package.md new file mode 100644 index 0000000000..524d7e2d9a --- /dev/null +++ b/.changeset/acp-package.md @@ -0,0 +1,8 @@ +--- +"@e2b/acp": minor +"@e2b/acp-python": minor +"e2b": patch +"@e2b/python-sdk": patch +--- + +Add `@e2b/acp` (JavaScript) and `e2b-acp` (Python) for running [Agent Client Protocol](https://agentclientprotocol.com) agents such as goose, OpenCode or Claude Code (through its ACP adapter) inside an E2B sandbox. `connectAcpAgent(sandbox, { command, client })` / `connect_acp_agent(sandbox, command, client)` starts the agent over stdin/stdout and returns the ACP client connection. Client filesystem and terminal capabilities are turned off and agent requests for them are rejected, so the agent edits files and runs commands with its own tools in the sandbox. The core SDKs can now stream a background command's output without keeping it in memory, which these packages use. From c0736a560c8e23264506f34c71af86bae8bdb0f8 Mon Sep 17 00:00:00 2001 From: Maksim Obukhov Date: Fri, 16 Oct 2026 09:41:00 +0200 Subject: [PATCH 11/26] chore: add acp-python to the pnpm lockfile --- pnpm-lock.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dcf0682b65..ce3af8bff0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -123,6 +123,12 @@ importers: specifier: 'catalog:' version: 4.1.11(@types/node@22.20.5)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(msw@2.15.0(@types/node@22.20.5)(@typescript/typescript6@6.0.2))(vite@6.4.3(@types/node@22.20.5)(jiti@2.7.0)(terser@5.46.0)(tsx@4.23.12)(yaml@2.9.1)) + packages/acp-python: + dependencies: + '@e2b/python-sdk': + specifier: workspace:* + version: link:../python-sdk + packages/cli: dependencies: '@iarna/toml': From b9850e6f67c7830209f744b23924a7b104de6a2e Mon Sep 17 00:00:00 2001 From: Maksim Obukhov Date: Fri, 16 Oct 2026 10:11:00 +0200 Subject: [PATCH 12/26] fix(acp-python): raise the e2b floor to the released core version --- packages/acp-python/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/acp-python/package.json b/packages/acp-python/package.json index c179310d12..118224e5cd 100644 --- a/packages/acp-python/package.json +++ b/packages/acp-python/package.json @@ -7,7 +7,7 @@ }, "scripts": { "test": "uv run pytest --verbose", - "postVersion": "uv version --frozen $(pnpm pkg get version --workspaces=false | tr -d \\\")", + "postVersion": "uv version --frozen $(pnpm pkg get version --workspaces=false | tr -d \\\") && uv add --frozen \"e2b>=$(node -p \"require('../python-sdk/package.json').version\"),<3.0.0\"", "lock": "uv lock", "postPublish": "uv build && uv publish --trusted-publishing always --check-url https://pypi.org/simple/", "typecheck": "uv run make typecheck", From 35981bf7edfb28f3ed99c461228879441ecc957e Mon Sep 17 00:00:00 2001 From: Maksim Obukhov Date: Fri, 16 Oct 2026 10:42:00 +0200 Subject: [PATCH 13/26] fix(acp-js): reject close() when the agent could not be killed --- packages/acp-js/src/index.ts | 2 +- packages/acp-js/src/transport.ts | 7 +++++-- packages/acp-js/tests/transport.offline.test.ts | 10 ++++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/acp-js/src/index.ts b/packages/acp-js/src/index.ts index fad72db78a..b55613867a 100644 --- a/packages/acp-js/src/index.ts +++ b/packages/acp-js/src/index.ts @@ -66,7 +66,7 @@ export class AcpAgent { /** * Close the agent: send EOF to its stdin and kill it if it has not exited * within `timeoutMs` of this call. Messages the agent sends after this call - * are not delivered. + * are not delivered. It rejects if the agent could not be killed. * * @param opts options for closing the agent. */ diff --git a/packages/acp-js/src/transport.ts b/packages/acp-js/src/transport.ts index 0983bf6d0e..bca04cb46f 100644 --- a/packages/acp-js/src/transport.ts +++ b/packages/acp-js/src/transport.ts @@ -106,6 +106,8 @@ export class AcpTransport { * Close the agent: send EOF to its stdin and kill it if it has not exited * within `timeoutMs` of this call. Messages the agent sends after this call * are not delivered. + * + * It rejects if the agent could not be killed. */ async close(opts?: AcpCloseOpts): Promise { // Only a known exit status means the process is gone. A failed output @@ -129,7 +131,7 @@ export class AcpTransport { clearTimeout(timer) if (!exited && !this.exitStatus()) { - await this.kill().catch(() => false) + await this.kill() } } @@ -292,7 +294,8 @@ export async function startAcpTransport( complete = lines.push(data) } catch (err) { end(err) - void closeTransport() + // The stream already failed with the real error. + closeTransport().catch(() => {}) return } for (const line of complete) { diff --git a/packages/acp-js/tests/transport.offline.test.ts b/packages/acp-js/tests/transport.offline.test.ts index 3a691a61a9..0e86138ac2 100644 --- a/packages/acp-js/tests/transport.offline.test.ts +++ b/packages/acp-js/tests/transport.offline.test.ts @@ -125,6 +125,16 @@ test('close timeout covers a hung EOF and then kills', async () => { expect(agent.handle.kill).toHaveBeenCalledOnce() }) +test('close rejects when the agent could not be killed', async () => { + const agent = fakeAgent({ closeStdin: () => new Promise(() => {}) }) + agent.handle.kill.mockRejectedValueOnce(new Error('kill failed')) + const transport = await startAcpTransport(agent.sandbox, 'agent') + + await expect(transport.close({ timeoutMs: 50 })).rejects.toThrow( + 'kill failed' + ) +}) + test('close keeps buffered messages and drops later ones', async () => { const agent = fakeAgent() const transport = await startAcpTransport(agent.sandbox, 'agent') From b5489cd3840bf6181b146661f3fb7bf652100a95 Mon Sep 17 00:00:00 2001 From: Maksim Obukhov Date: Fri, 16 Oct 2026 11:12:00 +0200 Subject: [PATCH 14/26] fix(acp-python): raise from close() when the agent could not be killed --- packages/acp-python/e2b_acp/__init__.py | 3 ++- packages/acp-python/e2b_acp/transport.py | 14 +++++++++----- .../acp-python/tests/test_transport_offline.py | 14 ++++++++++++++ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/packages/acp-python/e2b_acp/__init__.py b/packages/acp-python/e2b_acp/__init__.py index 1994672dc9..924af01205 100644 --- a/packages/acp-python/e2b_acp/__init__.py +++ b/packages/acp-python/e2b_acp/__init__.py @@ -49,7 +49,8 @@ async def close(self, timeout: float = 5) -> None: """ Close the agent: send EOF to its stdin and kill it if it has not exited within `timeout` of this call. Messages the agent sends after - this call are not delivered. + this call are not delivered. It raises if the agent could not be + killed. :param timeout: Time for the agent to exit in **seconds** """ diff --git a/packages/acp-python/e2b_acp/transport.py b/packages/acp-python/e2b_acp/transport.py index 753e426ebb..7013d676df 100644 --- a/packages/acp-python/e2b_acp/transport.py +++ b/packages/acp-python/e2b_acp/transport.py @@ -116,10 +116,7 @@ async def close(self, timeout: float = 5) -> None: except Exception: exit = None if exit is None and self._exit is None: - try: - await self.kill() - except Exception: - pass + await self.kill() async def kill(self) -> bool: assert self._handle is not None @@ -161,6 +158,13 @@ async def start( self._handle_ready.set_result(handle) self._exit_task = asyncio.create_task(self._watch_exit(handle)) + async def _close_quietly(self) -> None: + # The stream already failed with the real error. + try: + await self.close() + except Exception: + pass + async def _stop_and_wait(self) -> Optional[AcpAgentExit]: await self._end_input() assert self._exit_task is not None @@ -218,7 +222,7 @@ async def _handle_stdout(self, data: str) -> None: lines = self._lines.push(data) except SandboxException as e: self._end(e) - asyncio.ensure_future(self.close()) + asyncio.ensure_future(self._close_quietly()) return for line in lines: await self._deliver(line) diff --git a/packages/acp-python/tests/test_transport_offline.py b/packages/acp-python/tests/test_transport_offline.py index 6dc768810f..8c53506cca 100644 --- a/packages/acp-python/tests/test_transport_offline.py +++ b/packages/acp-python/tests/test_transport_offline.py @@ -3,6 +3,7 @@ import time from types import SimpleNamespace +import pytest from packaging.version import Version from e2b import CommandExitException @@ -140,6 +141,19 @@ async def test_close_timeout_covers_a_hung_eof_and_then_kills(): assert agent.killed == 1 +async def test_close_raises_when_the_agent_could_not_be_killed(): + agent = FakeAgent(close_stdin=hang) + + async def fail(): + raise RuntimeError("kill failed") + + agent.handle.kill = fail + transport = await start(agent) + + with pytest.raises(RuntimeError, match="kill failed"): + await transport.close(timeout=0.05) + + async def test_close_keeps_buffered_messages_and_drops_later_ones(): agent = FakeAgent() transport = await start(agent) From ce191b8167c7096328999bc32853391ba201e61f Mon Sep 17 00:00:00 2001 From: Maksim Obukhov Date: Fri, 16 Oct 2026 11:43:00 +0200 Subject: [PATCH 15/26] fix(acp-js): kill the agent when the client factory throws --- packages/acp-js/src/index.ts | 12 ++++++++---- packages/acp-js/tests/transport.offline.test.ts | 17 ++++++++++++++++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/acp-js/src/index.ts b/packages/acp-js/src/index.ts index b55613867a..825ebb9f38 100644 --- a/packages/acp-js/src/index.ts +++ b/packages/acp-js/src/index.ts @@ -119,8 +119,12 @@ export async function connectAcpAgent( const { command, client, ...startOpts } = opts const transport = await startAcpTransport(sandbox, command, startOpts) const toClient = typeof client === 'function' ? client : () => client - return new AcpAgent( - transport, - new ClientSideConnection(toClient, transport.stream) - ) + let connection: ClientSideConnection + try { + connection = new ClientSideConnection(toClient, transport.stream) + } catch (err) { + await transport.kill().catch(() => false) + throw err + } + return new AcpAgent(transport, connection) } diff --git a/packages/acp-js/tests/transport.offline.test.ts b/packages/acp-js/tests/transport.offline.test.ts index 0e86138ac2..77973d8084 100644 --- a/packages/acp-js/tests/transport.offline.test.ts +++ b/packages/acp-js/tests/transport.offline.test.ts @@ -1,5 +1,6 @@ -import { CommandExitError, type CommandStartOpts } from 'e2b' +import { CommandExitError, type CommandStartOpts, type Sandbox } from 'e2b' import { assert, expect, test, vi } from 'vitest' +import { connectAcpAgent } from '../src' import { startAcpTransport, type AcpSandbox } from '../src/transport' const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)) @@ -152,3 +153,17 @@ test('close keeps buffered messages and drops later ones', async () => { ) assert.isTrue((await reader.read()).done) }) + +test('connect kills the agent when the client factory throws', async () => { + const agent = fakeAgent() + + await expect( + connectAcpAgent(agent.sandbox as Sandbox, { + command: 'agent', + client: () => { + throw new Error('no client') + }, + }) + ).rejects.toThrow('no client') + expect(agent.handle.kill).toHaveBeenCalledOnce() +}) From fb38bea2f1bcd608ffc0f108a1be1d97a577548c Mon Sep 17 00:00:00 2001 From: Maksim Obukhov Date: Fri, 16 Oct 2026 12:13:00 +0200 Subject: [PATCH 16/26] test(acp-js): cover dropping in-flight lines after close --- .../acp-js/tests/transport.offline.test.ts | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/packages/acp-js/tests/transport.offline.test.ts b/packages/acp-js/tests/transport.offline.test.ts index 77973d8084..e837126b73 100644 --- a/packages/acp-js/tests/transport.offline.test.ts +++ b/packages/acp-js/tests/transport.offline.test.ts @@ -51,8 +51,10 @@ function fakeAgent(opts: { closeStdin?: () => Promise } = {}) { handle, writes, maxActive: () => maxActive, - emit: (message: unknown) => - callbacks.onStdout!(JSON.stringify(message) + '\n'), + emit: (...messages: unknown[]) => + callbacks.onStdout!( + messages.map((m) => JSON.stringify(m) + '\n').join('') + ), exitWith, fail: (err: unknown) => settle.reject(err), } @@ -154,6 +156,31 @@ test('close keeps buffered messages and drops later ones', async () => { assert.isTrue((await reader.read()).done) }) +test('close drops later lines of an output chunk already being read', async () => { + const agent = fakeAgent() + const transport = await startAcpTransport(agent.sandbox, 'agent') + + for (let i = 0; i < 63; i++) { + await agent.emit({ jsonrpc: '2.0', method: 'before' }) + } + // The read queue fills on the first line, so the chunk waits for the client. + const reading = agent.emit( + { jsonrpc: '2.0', method: 'before' }, + { jsonrpc: '2.0', method: 'after' } + ) + await sleep(0) + await transport.close({ timeoutMs: 10 }) + await reading + + const methods: string[] = [] + const reader = transport.stream.readable.getReader() + for (let r = await reader.read(); !r.done; r = await reader.read()) { + methods.push((r.value as { method: string }).method) + } + assert.lengthOf(methods, 64) + assert.notInclude(methods, 'after') +}) + test('connect kills the agent when the client factory throws', async () => { const agent = fakeAgent() From d44f4b41981e8d95d8110c55055132d76fdc7906 Mon Sep 17 00:00:00 2001 From: Maksim Obukhov Date: Fri, 16 Oct 2026 12:44:00 +0200 Subject: [PATCH 17/26] docs(acp-python): describe how requests fail when the agent exits --- packages/acp-python/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/acp-python/README.md b/packages/acp-python/README.md index b3484e27e4..a6ce9317c8 100644 --- a/packages/acp-python/README.md +++ b/packages/acp-python/README.md @@ -73,5 +73,5 @@ The agent talks ACP over its stdin and stdout. `connect_acp_agent` turns off the - `agent.close()` sends EOF to the agent and kills it if it has not exited within `timeout` (5 seconds by default) of the call. Messages the agent sends after that are not delivered. - `agent.wait()` returns the exit code once the agent exits. -- If the agent exits with an error, the connection fails with the exit code and the end of the agent's stderr. Pass `on_stderr` to receive its logs. +- If the agent exits, pending requests on the connection fail with a connection error. Use `agent.wait()` for the exit code and pass `on_stderr` to receive the agent's logs. - The agent runs as long as the sandbox does, so set the sandbox timeout for the longest task you expect. From b46abdaadac7df56c8a0cf26de1bacba4bd4188d Mon Sep 17 00:00:00 2001 From: Maksim Obukhov Date: Fri, 16 Oct 2026 13:14:00 +0200 Subject: [PATCH 18/26] fix(acp-python): drop the unused xdist flag from make test --- packages/acp-python/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/acp-python/Makefile b/packages/acp-python/Makefile index eba07466b1..d8810ea2ec 100644 --- a/packages/acp-python/Makefile +++ b/packages/acp-python/Makefile @@ -10,4 +10,4 @@ format: .PHONY: test test: - uv run pytest --verbose --numprocesses=4 + uv run pytest --verbose From 6e469da7ce7dc2c2d321881d73e4fe977cf0d4bb Mon Sep 17 00:00:00 2001 From: Maksim Obukhov Date: Fri, 16 Oct 2026 13:45:00 +0200 Subject: [PATCH 19/26] test(acp-js): drop live tests the offline tests already cover --- packages/acp-js/tests/fakeAgent.ts | 15 ---- packages/acp-js/tests/transport.test.ts | 92 ------------------------- 2 files changed, 107 deletions(-) diff --git a/packages/acp-js/tests/fakeAgent.ts b/packages/acp-js/tests/fakeAgent.ts index d3393ca036..4cc30cf7bf 100644 --- a/packages/acp-js/tests/fakeAgent.ts +++ b/packages/acp-js/tests/fakeAgent.ts @@ -25,8 +25,6 @@ for line in sys.stdin: continue if method == "initialize": send({"jsonrpc": "2.0", "id": id, "result": {"protocolVersion": 1, "received": params}}) - elif method == "test/echo": - send({"jsonrpc": "2.0", "id": id, "result": params}) elif method == "test/client_call": # Ask the client to run something on its machine. pending["call-" + str(id)] = id @@ -47,23 +45,10 @@ for line in sys.stdin: send({"jsonrpc": "2.0", "id": id, "result": errors}) elif method == "test/big": send({"jsonrpc": "2.0", "id": id, "result": "x" * params["size"]}) - elif method == "test/stderr_then_sleep": - sys.stderr.write("log line\n") - sys.stderr.flush() - send({"jsonrpc": "2.0", "id": id, "result": "sleeping"}) - time.sleep(600) elif method == "test/exit": sys.stderr.write(params.get("stderr", "")) sys.stderr.flush() sys.exit(params["code"]) - elif method == "test/flood": - for i in range(params["count"]): - send({"jsonrpc": "2.0", "method": "session/update", "params": {"i": i}}) - send({"jsonrpc": "2.0", "id": id, "result": "flooded"}) - elif method == "test/ignore_eof": - os.close(0) - send({"jsonrpc": "2.0", "id": id, "result": "ignoring"}) - time.sleep(600) ` export const FAKE_AGENT_PATH = '/tmp/fake_acp_agent.py' diff --git a/packages/acp-js/tests/transport.test.ts b/packages/acp-js/tests/transport.test.ts index dc40dae299..6aaf583bb0 100644 --- a/packages/acp-js/tests/transport.test.ts +++ b/packages/acp-js/tests/transport.test.ts @@ -66,23 +66,6 @@ sandboxTest( } ) -sandboxTest('writes keep their order', async ({ sandbox }) => { - const { agent, writer, reader } = await startAgent(sandbox) - const count = 100 - - await Promise.all( - Array.from({ length: count }, (_, i) => - writer.write({ jsonrpc: '2.0', id: i, method: 'test/echo', params: i }) - ) - ) - - for (let i = 0; i < count; i++) { - const { value } = await reader.read() - assert.equal((value as Message).result, i) - } - await agent.close() -}) - sandboxTest( 'messages split into single bytes are reassembled', async ({ sandbox }) => { @@ -145,24 +128,6 @@ sandboxTest( } ) -sandboxTest( - 'close kills an agent that ignores EOF', - async ({ sandbox }) => { - const { agent, call, reader } = await startAgent(sandbox) - assert.equal((await call(8, 'test/ignore_eof')).result, 'ignoring') - - const started = Date.now() - await agent.close({ timeoutMs: 1_000 }) - - assert.isBelow(Date.now() - started, 10_000) - const { done } = await reader.read() - assert.isTrue(done) - const processes = await sandbox.commands.list() - assert.isFalse(processes.some((p) => p.pid === agent.pid)) - }, - 30_000 -) - sandboxTest( 'agent failure errors the stream with its stderr', async ({ sandbox }) => { @@ -199,60 +164,3 @@ sandboxTest('cancelling the readable closes the agent', async ({ sandbox }) => { assert.equal((await agent.wait()).exitCode, 0) }) - -sandboxTest( - 'close does not hang when the client stops reading', - async ({ sandbox }) => { - const { agent, writer } = await startAgent(sandbox) - - // Far more messages than the read queue holds, none of them read. - await writer.write({ - jsonrpc: '2.0', - id: 10, - method: 'test/flood', - params: { count: 1000 }, - }) - await new Promise((resolve) => setTimeout(resolve, 2_000)) - - await agent.close({ timeoutMs: 2_000 }) - - assert.equal((await agent.wait()).exitCode, 0) - }, - 30_000 -) - -sandboxTest( - 'close stops the agent when the stderr callback failed', - async ({ sandbox }) => { - const { agent, writer, reader } = await startAgent(sandbox, { - onStderr: () => { - throw new Error('callback failed') - }, - }) - // The agent logs to stderr and keeps running. - await writer.write({ - jsonrpc: '2.0', - id: 11, - method: 'test/stderr_then_sleep', - params: {}, - }) - - // The failed callback ends the output stream, but not the process. - let error: unknown - try { - await reader.read() - } catch (err) { - error = err - } - assert.match(String(error), /callback failed/) - assert.isTrue( - (await sandbox.commands.list()).some((p) => p.pid === agent.pid) - ) - - await agent.close({ timeoutMs: 1_000 }) - - const processes = await sandbox.commands.list() - assert.isFalse(processes.some((p) => p.pid === agent.pid)) - }, - 30_000 -) From fe004fef4e9686a8ce750848e3b7b41da7c4b8da Mon Sep 17 00:00:00 2001 From: Maksim Obukhov Date: Fri, 16 Oct 2026 14:14:00 +0200 Subject: [PATCH 20/26] test(acp-python): drop live tests the offline tests already cover --- packages/acp-python/tests/acp_fake_agent.py | 15 ----- packages/acp-python/tests/test_transport.py | 69 --------------------- 2 files changed, 84 deletions(-) diff --git a/packages/acp-python/tests/acp_fake_agent.py b/packages/acp-python/tests/acp_fake_agent.py index 55bc01d9de..d940e3b24d 100644 --- a/packages/acp-python/tests/acp_fake_agent.py +++ b/packages/acp-python/tests/acp_fake_agent.py @@ -27,8 +27,6 @@ def send(message): continue if method == "initialize": send({"jsonrpc": "2.0", "id": id, "result": {"protocolVersion": 1, "received": params}}) - elif method == "test/echo": - send({"jsonrpc": "2.0", "id": id, "result": params}) elif method == "test/client_call": # Ask the client to run something on its machine. pending["call-" + str(id)] = id @@ -49,23 +47,10 @@ def send(message): send({"jsonrpc": "2.0", "id": id, "result": errors}) elif method == "test/big": send({"jsonrpc": "2.0", "id": id, "result": "x" * params["size"]}) - elif method == "test/stderr_then_sleep": - sys.stderr.write("log line\n") - sys.stderr.flush() - send({"jsonrpc": "2.0", "id": id, "result": "sleeping"}) - time.sleep(600) elif method == "test/exit": sys.stderr.write(params.get("stderr", "")) sys.stderr.flush() sys.exit(params["code"]) - elif method == "test/flood": - for i in range(params["count"]): - send({"jsonrpc": "2.0", "method": "session/update", "params": {"i": i}}) - send({"jsonrpc": "2.0", "id": id, "result": "flooded"}) - elif method == "test/ignore_eof": - os.close(0) - send({"jsonrpc": "2.0", "id": id, "result": "ignoring"}) - time.sleep(600) """ FAKE_AGENT_PATH = "/tmp/fake_acp_agent.py" diff --git a/packages/acp-python/tests/test_transport.py b/packages/acp-python/tests/test_transport.py index a62bb6247a..58a417d6ca 100644 --- a/packages/acp-python/tests/test_transport.py +++ b/packages/acp-python/tests/test_transport.py @@ -1,6 +1,3 @@ -import asyncio -import time - import pytest from e2b import AsyncSandbox @@ -71,22 +68,6 @@ async def test_agent_requests_for_client_fs_and_terminal_are_rejected( await agent.close() -async def test_writes_keep_their_order(async_sandbox: AsyncSandbox): - agent, _ = await start_agent(async_sandbox) - count = 100 - - await asyncio.gather( - *( - agent.send({"jsonrpc": "2.0", "id": i, "method": "test/echo", "params": i}) - for i in range(count) - ) - ) - - for i in range(count): - assert (await agent.receive())["result"] == i - await agent.close() - - async def test_messages_split_into_single_bytes_are_reassembled( async_sandbox: AsyncSandbox, ): @@ -127,19 +108,6 @@ async def test_close_sends_eof_and_ends_the_stream(async_sandbox: AsyncSandbox): assert exit.exit_code == 0 -async def test_close_kills_an_agent_that_ignores_eof(async_sandbox: AsyncSandbox): - agent, call = await start_agent(async_sandbox) - assert (await call(8, "test/ignore_eof"))["result"] == "ignoring" - - started = time.monotonic() - await agent.close(timeout=1) - - assert time.monotonic() - started < 10 - assert await agent.receive() is None - processes = await async_sandbox.commands.list() - assert all(p.pid != agent.pid for p in processes) - - async def test_agent_failure_errors_the_stream_with_its_stderr( async_sandbox: AsyncSandbox, ): @@ -150,40 +118,3 @@ async def test_agent_failure_errors_the_stream_with_its_stderr( await call(9, "test/exit", {"code": 3, "stderr": "boom"}) assert "".join(stderr) == "boom" assert (await agent.wait()).exit_code == 3 - - -async def test_close_does_not_hang_when_the_client_stops_reading( - async_sandbox: AsyncSandbox, -): - agent, _ = await start_agent(async_sandbox) - - # Far more messages than the read queue holds, none of them read. - await agent.send( - {"jsonrpc": "2.0", "id": 10, "method": "test/flood", "params": {"count": 1000}} - ) - await asyncio.sleep(2) - - await agent.close(timeout=2) - - assert (await agent.wait()).exit_code == 0 - - -async def test_close_stops_the_agent_when_the_stderr_callback_failed( - async_sandbox: AsyncSandbox, -): - def fail(data): - raise RuntimeError("callback failed") - - agent, _ = await start_agent(async_sandbox, on_stderr=fail) - await agent.send( - {"jsonrpc": "2.0", "id": 11, "method": "test/stderr_then_sleep", "params": {}} - ) - - # The failed callback ends the output stream, but not the process. - with pytest.raises(Exception, match="callback failed"): - await agent.receive() - assert any(p.pid == agent.pid for p in await async_sandbox.commands.list()) - - await agent.close(timeout=1) - - assert all(p.pid != agent.pid for p in await async_sandbox.commands.list()) From cb4bd67ed8192da991b87c1ba6cea4c4767cd04f Mon Sep 17 00:00:00 2001 From: Maksim Obukhov Date: Mon, 19 Oct 2026 10:02:41 +0200 Subject: [PATCH 21/26] fix(acp-js): let kill() and early output errors stop the agent kill() now releases agent output that waits for the client, so the command handle can see the exit and wait() resolves. Cleanup after an oversized first message waits for the transport instead of doing nothing when the output arrives before startStreaming() returns. --- packages/acp-js/src/transport.ts | 18 ++++--- .../acp-js/tests/transport.offline.test.ts | 54 +++++++++++++++++-- 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/packages/acp-js/src/transport.ts b/packages/acp-js/src/transport.ts index bca04cb46f..2ae3625132 100644 --- a/packages/acp-js/src/transport.ts +++ b/packages/acp-js/src/transport.ts @@ -90,7 +90,8 @@ export class AcpTransport { private readonly handle: CommandHandle, private readonly exit: Promise, private readonly exitStatus: () => AcpAgentExit | undefined, - private readonly stop: () => Promise + private readonly stopReading: () => void, + private readonly endInput: () => Promise ) {} /** @@ -116,9 +117,10 @@ export class AcpTransport { return } + this.stopReading() let timer: ReturnType | undefined const exited = await Promise.race([ - this.stop() + this.endInput() .then(() => this.exit) .then( () => true, @@ -141,6 +143,9 @@ export class AcpTransport { * @returns `true` if the agent was killed, `false` if it was not found. */ async kill(): Promise { + // Output waiting for the client would otherwise keep the handle from + // seeing the exit. + this.stopReading() return this.handle.kill() } } @@ -241,7 +246,8 @@ export async function startAcpTransport( } let transport: AcpTransport | undefined - const closeTransport = () => transport?.close() ?? Promise.resolve() + // Output can arrive before startStreaming() returns, so wait for the transport. + const closeTransport = () => handleReady.then(() => transport!.close()) const readable = new ReadableStream( { @@ -269,11 +275,10 @@ export async function startAcpTransport( abort: () => closeTransport(), }) - const stop = () => { + const stopReading = () => { closing = true resumeReading?.() resumeReading = undefined - return endInput() } let handle: CommandHandle @@ -351,7 +356,8 @@ export async function startAcpTransport( handle, exit, () => exitStatus, - stop + stopReading, + endInput ) return transport } diff --git a/packages/acp-js/tests/transport.offline.test.ts b/packages/acp-js/tests/transport.offline.test.ts index e837126b73..f27f417efd 100644 --- a/packages/acp-js/tests/transport.offline.test.ts +++ b/packages/acp-js/tests/transport.offline.test.ts @@ -9,7 +9,12 @@ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)) function fakeAgent(opts: { closeStdin?: () => Promise } = {}) { let callbacks!: Pick let settle!: { resolve: (r: unknown) => void; reject: (e: unknown) => void } - const exit = new Promise((resolve, reject) => (settle = { resolve, reject })) + const settled = new Promise( + (resolve, reject) => (settle = { resolve, reject }) + ) + // Like the SDK handle, the exit is seen only after output callbacks return. + let output: Promise = Promise.resolve() + const exit = settled.finally(() => output) exit.catch(() => {}) const writes: string[] = [] @@ -52,9 +57,11 @@ function fakeAgent(opts: { closeStdin?: () => Promise } = {}) { writes, maxActive: () => maxActive, emit: (...messages: unknown[]) => - callbacks.onStdout!( - messages.map((m) => JSON.stringify(m) + '\n').join('') - ), + (output = Promise.resolve( + callbacks.onStdout!( + messages.map((m) => JSON.stringify(m) + '\n').join('') + ) + )), exitWith, fail: (err: unknown) => settle.reject(err), } @@ -194,3 +201,42 @@ test('connect kills the agent when the client factory throws', async () => { ).rejects.toThrow('no client') expect(agent.handle.kill).toHaveBeenCalledOnce() }) + +test('kill lets wait() see the exit while output waits for the client', async () => { + const agent = fakeAgent() + const transport = await startAcpTransport(agent.sandbox, 'agent') + + for (let i = 0; i < 63; i++) { + await agent.emit({ jsonrpc: '2.0', method: 'update' }) + } + // The read queue is full, so this callback waits for the client. + agent.emit({ jsonrpc: '2.0', method: 'update' }) + await sleep(0) + await transport.kill() + + const exited = await Promise.race([ + transport.wait().then(() => true), + sleep(500).then(() => false), + ]) + assert.isTrue(exited) +}) + +test('output over the limit before start returns still closes the agent', async () => { + const closeStdin = vi.fn(async () => {}) + const agent = fakeAgent({ closeStdin }) + const startStreaming = agent.sandbox.commands.startStreaming + agent.sandbox.commands.startStreaming = async (cmd, opts) => { + const handle = await startStreaming(cmd, opts) + opts!.onStdout!('x'.repeat(100)) + return handle + } + const transport = await startAcpTransport(agent.sandbox, 'agent', { + maxMessageBytes: 10, + }) + + await expect(transport.stream.readable.getReader().read()).rejects.toThrow( + /exceeds the limit/ + ) + await sleep(0) + expect(closeStdin).toHaveBeenCalledOnce() +}) From e18515b6068736177ee7a75bb4935704a0d5439d Mon Sep 17 00:00:00 2001 From: Maksim Obukhov Date: Mon, 19 Oct 2026 10:02:41 +0200 Subject: [PATCH 22/26] fix(acp-js): split agent output without rescanning a pending line The line splitter kept the unfinished line as one string and split it again on every chunk, which made a long message quadratic to read. It now keeps the chunks and their byte count and joins them once. --- packages/acp-js/src/utils.ts | 52 ++++++++++++++++++++--------- packages/acp-js/tests/utils.test.ts | 12 +++++++ 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/packages/acp-js/src/utils.ts b/packages/acp-js/src/utils.ts index 28cf86e7e3..6d0be2f267 100644 --- a/packages/acp-js/src/utils.ts +++ b/packages/acp-js/src/utils.ts @@ -12,7 +12,10 @@ const encoder = new TextEncoder() * Splits newline-delimited text chunks into complete lines. */ export class LineSplitter { - private pending = '' + // Text after the last newline. Chunks are joined once, when the line ends, + // so a long message is not rescanned on every chunk. + private pending: string[] = [] + private pendingBytes = 0 constructor(private readonly maxLineBytes: number) { if (!Number.isSafeInteger(maxLineBytes) || maxLineBytes <= 0) { @@ -23,28 +26,45 @@ export class LineSplitter { } push(chunk: string): string[] { - const parts = (this.pending + chunk).split('\n') - this.pending = parts.pop() ?? '' - // Characters never outnumber UTF-8 bytes, so the cheap length check only - // lets through lines that might be too large. - for (const line of [...parts, this.pending]) { - if ( - line.length > this.maxLineBytes / 4 && - encoder.encode(line).length > this.maxLineBytes - ) { - throw new SandboxError( - `ACP message exceeds the limit of ${this.maxLineBytes} bytes` - ) + const lines = chunk.split('\n') + const rest = lines.pop() ?? '' + if (lines.length > 0) { + this.append(lines[0]) + lines[0] = this.flush() + for (const line of lines.slice(1)) { + // Characters never outnumber UTF-8 bytes, so the cheap length check + // only lets through lines that might be too large. + if (line.length > this.maxLineBytes / 4) { + this.check(encoder.encode(line).length) + } } } - return parts + this.append(rest) + return lines } flush(): string { - const rest = this.pending - this.pending = '' + const rest = this.pending.join('') + this.pending = [] + this.pendingBytes = 0 return rest } + + private append(text: string) { + if (text) { + this.pending.push(text) + this.pendingBytes += encoder.encode(text).length + this.check(this.pendingBytes) + } + } + + private check(bytes: number) { + if (bytes > this.maxLineBytes) { + throw new SandboxError( + `ACP message exceeds the limit of ${this.maxLineBytes} bytes` + ) + } + } } function isRecord(value: unknown): value is Record { diff --git a/packages/acp-js/tests/utils.test.ts b/packages/acp-js/tests/utils.test.ts index 827f99faf3..26e6c25ac5 100644 --- a/packages/acp-js/tests/utils.test.ts +++ b/packages/acp-js/tests/utils.test.ts @@ -29,6 +29,18 @@ test('line splitter rejects messages over the limit in bytes', () => { expect(() => lines.push('€€€€')).toThrow(/exceeds the limit of 8 bytes/) }) +test('line splitter handles a long message in small chunks quickly', () => { + const lines = new LineSplitter(32 * 1024 * 1024) + const chunk = 'x'.repeat(1024) + + const started = Date.now() + for (let i = 0; i < 8 * 1024; i++) { + assert.deepEqual(lines.push(chunk), []) + } + assert.lengthOf(lines.push('\n')[0], 8 * 1024 * 1024) + assert.isBelow(Date.now() - started, 1_000) +}) + test('initialize turns off client filesystem and terminal', () => { const message = { jsonrpc: '2.0', From 431618e5e889848152a467bfb6fa6acbbdb9674d Mon Sep 17 00:00:00 2001 From: Maksim Obukhov Date: Mon, 19 Oct 2026 10:03:40 +0200 Subject: [PATCH 23/26] fix(acp-python): let kill() stop output that waits for the client kill() now releases agent output that waits for room in the read queue, so the command handle can see the exit and wait() returns. --- packages/acp-python/e2b_acp/transport.py | 4 +++ .../tests/test_transport_offline.py | 28 +++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/acp-python/e2b_acp/transport.py b/packages/acp-python/e2b_acp/transport.py index 7013d676df..5f5ec349d0 100644 --- a/packages/acp-python/e2b_acp/transport.py +++ b/packages/acp-python/e2b_acp/transport.py @@ -120,6 +120,10 @@ async def close(self, timeout: float = 5) -> None: async def kill(self) -> bool: assert self._handle is not None + # Output waiting for the client would otherwise keep the handle from + # seeing the exit. + self._closing = True + self._space.set() return await self._handle.kill() async def wait(self) -> AcpAgentExit: diff --git a/packages/acp-python/tests/test_transport_offline.py b/packages/acp-python/tests/test_transport_offline.py index 8c53506cca..f102aa6e65 100644 --- a/packages/acp-python/tests/test_transport_offline.py +++ b/packages/acp-python/tests/test_transport_offline.py @@ -19,6 +19,8 @@ def __init__(self, close_stdin=None): self.max_active = 0 self.killed = 0 self.exit = asyncio.get_running_loop().create_future() + # Like the SDK handle, the exit is seen only after output callbacks return. + self._output = asyncio.Lock() self._close_stdin = close_stdin self.handle = SimpleNamespace( pid=42, @@ -52,7 +54,11 @@ async def _kill(self): return True async def _wait(self): - return await asyncio.shield(self.exit) + try: + return await asyncio.shield(self.exit) + finally: + async with self._output: + pass def exit_with(self, code): if self.exit.done(): @@ -64,8 +70,9 @@ def exit_with(self, code): CommandExitException(stdout="", stderr="", exit_code=code, error=None) ) - def emit(self, message): - return self.on_stdout(json.dumps(message) + "\n") + async def emit(self, message): + async with self._output: + await self.on_stdout(json.dumps(message) + "\n") async def start(agent): @@ -190,3 +197,18 @@ async def test_close_releases_a_reader_blocked_on_a_full_queue(): received.append(message["method"]) assert "late" not in received assert len(received) == READ_QUEUE_MESSAGES + + +async def test_kill_lets_wait_see_the_exit_while_output_waits_for_the_client(): + agent = FakeAgent() + transport = await start(agent) + for i in range(READ_QUEUE_MESSAGES): + await agent.emit({"jsonrpc": "2.0", "method": "update", "params": i}) + + # The read queue is full, so this callback waits for the client. + asyncio.create_task(agent.emit({"jsonrpc": "2.0", "method": "late"})) + await asyncio.sleep(0.01) + await transport.kill() + + exit = await asyncio.wait_for(transport.wait(), 1) + assert exit.exit_code == -1 From d1c9d86b8630d5ca29a353fcd525211e86362555 Mon Sep 17 00:00:00 2001 From: Maksim Obukhov Date: Mon, 19 Oct 2026 10:03:40 +0200 Subject: [PATCH 24/26] fix(acp-python): split agent output without rescanning a pending line The line splitter kept the unfinished line as one string and split it again on every chunk, which made a long message quadratic to read. It now keeps the chunks and their byte count and joins them once. --- packages/acp-python/e2b_acp/utils.py | 46 +++++++++++++++++-------- packages/acp-python/tests/test_utils.py | 13 +++++++ 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/packages/acp-python/e2b_acp/utils.py b/packages/acp-python/e2b_acp/utils.py index 4f16fb080e..bc1236ea53 100644 --- a/packages/acp-python/e2b_acp/utils.py +++ b/packages/acp-python/e2b_acp/utils.py @@ -39,27 +39,43 @@ def __init__(self, max_line_bytes: int): f"max_message_bytes must be a positive integer, got {max_line_bytes}" ) self._max_line_bytes = int(max_line_bytes) - self._pending = "" + # Text after the last newline. Chunks are joined once, when the line + # ends, so a long message is not rescanned on every chunk. + self._pending: List[str] = [] + self._pending_bytes = 0 def push(self, chunk: str) -> List[str]: - parts = (self._pending + chunk).split("\n") - self._pending = parts.pop() - # Characters never outnumber UTF-8 bytes, so the cheap length check - # only lets through lines that might be too large. - for line in [*parts, self._pending]: - if ( - len(line) > self._max_line_bytes / 4 - and len(line.encode()) > self._max_line_bytes - ): - raise SandboxException( - f"ACP message exceeds the limit of {self._max_line_bytes} bytes" - ) - return parts + lines = chunk.split("\n") + rest = lines.pop() + if lines: + self._append(lines[0]) + lines[0] = self.flush() + for line in lines[1:]: + # Characters never outnumber UTF-8 bytes, so the cheap length + # check only lets through lines that might be too large. + if len(line) > self._max_line_bytes / 4: + self._check(len(line.encode())) + self._append(rest) + return lines def flush(self) -> str: - rest, self._pending = self._pending, "" + rest = "".join(self._pending) + self._pending = [] + self._pending_bytes = 0 return rest + def _append(self, text: str) -> None: + if text: + self._pending.append(text) + self._pending_bytes += len(text.encode()) + self._check(self._pending_bytes) + + def _check(self, size: int) -> None: + if size > self._max_line_bytes: + raise SandboxException( + f"ACP message exceeds the limit of {self._max_line_bytes} bytes" + ) + def encode_message(message: Any) -> str: return json.dumps(message, separators=(",", ":"), ensure_ascii=False) + "\n" diff --git a/packages/acp-python/tests/test_utils.py b/packages/acp-python/tests/test_utils.py index dbaa896648..84769e0e80 100644 --- a/packages/acp-python/tests/test_utils.py +++ b/packages/acp-python/tests/test_utils.py @@ -1,3 +1,5 @@ +import time + import pytest from e2b import InvalidArgumentException, SandboxException @@ -26,6 +28,17 @@ def test_line_splitter_keeps_trailing_partial_line_for_flush(): assert lines.flush() == '{"b":2}' +def test_line_splitter_handles_a_long_message_in_small_chunks_quickly(): + lines = LineSplitter(32 * 1024 * 1024) + chunk = "x" * 1024 + + started = time.monotonic() + for _ in range(8 * 1024): + assert lines.push(chunk) == [] + assert len(lines.push("\n")[0]) == 8 * 1024 * 1024 + assert time.monotonic() - started < 1 + + def test_line_splitter_rejects_messages_over_the_limit_in_bytes(): lines = LineSplitter(8) From 833990b482fdbc55c6d0a087183426ec68c13a86 Mon Sep 17 00:00:00 2001 From: Maksim Obukhov Date: Mon, 19 Oct 2026 10:04:40 +0200 Subject: [PATCH 25/26] refactor(acp-js): take command and client as arguments, rename close() to stop() connectAcpAgent(sandbox, command, client, opts?) matches the Python signature and keeps only optional settings in the options object. stop() ends the agent process that connectAcpAgent started, its default timeout is the exported DEFAULT_STOP_TIMEOUT_MS, and AcpStartOpts forwards an AbortSignal to the start request. --- .changeset/acp-package.md | 2 +- packages/acp-js/README.md | 8 ++- packages/acp-js/src/index.ts | 51 +++++++++---------- packages/acp-js/src/transport.ts | 23 ++++++--- packages/acp-js/tests/connect.test.ts | 7 +-- .../acp-js/tests/transport.offline.test.ts | 7 +-- 6 files changed, 48 insertions(+), 50 deletions(-) diff --git a/.changeset/acp-package.md b/.changeset/acp-package.md index 524d7e2d9a..cf23537b62 100644 --- a/.changeset/acp-package.md +++ b/.changeset/acp-package.md @@ -5,4 +5,4 @@ "@e2b/python-sdk": patch --- -Add `@e2b/acp` (JavaScript) and `e2b-acp` (Python) for running [Agent Client Protocol](https://agentclientprotocol.com) agents such as goose, OpenCode or Claude Code (through its ACP adapter) inside an E2B sandbox. `connectAcpAgent(sandbox, { command, client })` / `connect_acp_agent(sandbox, command, client)` starts the agent over stdin/stdout and returns the ACP client connection. Client filesystem and terminal capabilities are turned off and agent requests for them are rejected, so the agent edits files and runs commands with its own tools in the sandbox. The core SDKs can now stream a background command's output without keeping it in memory, which these packages use. +Add `@e2b/acp` (JavaScript) and `e2b-acp` (Python) for running [Agent Client Protocol](https://agentclientprotocol.com) agents such as goose, OpenCode or Claude Code (through its ACP adapter) inside an E2B sandbox. `connectAcpAgent(sandbox, command, client)` / `connect_acp_agent(sandbox, command, client)` starts the agent over stdin/stdout and returns the ACP client connection. Client filesystem and terminal capabilities are turned off and agent requests for them are rejected, so the agent edits files and runs commands with its own tools in the sandbox. The core SDKs can now stream a background command's output without keeping it in memory, which these packages use. diff --git a/packages/acp-js/README.md b/packages/acp-js/README.md index 91a8c3e04e..7418de7ea7 100644 --- a/packages/acp-js/README.md +++ b/packages/acp-js/README.md @@ -35,9 +35,7 @@ const client: Client = { const sandbox = await Sandbox.create('goose', { timeoutMs: 10 * 60_000 }) try { - const agent = await connectAcpAgent(sandbox, { - command: 'goose acp --with-builtin developer', - client, + const agent = await connectAcpAgent(sandbox, 'goose acp --with-builtin developer', client, { envs: { GOOSE_PROVIDER: 'openai', GOOSE_MODEL: 'gpt-5.6-luna', @@ -52,7 +50,7 @@ try { prompt: [{ type: 'text', text: 'Create fizzbuzz.py for 1 to 15 and run it.' }], }) - await agent.close() + await agent.stop() } finally { await sandbox.kill() } @@ -64,7 +62,7 @@ The agent talks ACP over its stdin and stdout. `connectAcpAgent` turns off the c ## Lifecycle -- `agent.close()` sends EOF to the agent and kills it if it has not exited within `timeoutMs` (5 seconds by default) of the call. Messages the agent sends after that are not delivered. +- `agent.stop()` sends EOF to the agent and kills it if it has not exited within `timeoutMs` (5 seconds by default) of the call. Messages the agent sends after that are not delivered. - `agent.wait()` resolves with the exit code once the agent exits. - If the agent exits with an error, the connection fails with the exit code and the end of the agent's stderr. Pass `onStderr` to receive its logs. - The agent runs as long as the sandbox does, so set the sandbox timeout for the longest task you expect. diff --git a/packages/acp-js/src/index.ts b/packages/acp-js/src/index.ts index 825ebb9f38..41886cea23 100644 --- a/packages/acp-js/src/index.ts +++ b/packages/acp-js/src/index.ts @@ -7,27 +7,13 @@ import type { Sandbox } from 'e2b' import { startAcpTransport, type AcpAgentExit, - type AcpCloseOpts, type AcpStartOpts, + type AcpStopOpts, type AcpTransport, } from './transport' -export type { AcpAgentExit, AcpCloseOpts, AcpStartOpts } from './transport' - -/** - * Options for connecting to an ACP agent in a sandbox. - */ -export interface ConnectAcpAgentOpts extends AcpStartOpts { - /** - * Command that starts the agent in ACP mode, for example `goose acp`. - */ - command: string - /** - * Your ACP client, or a function that creates it for the agent connection. - * It answers permission requests and receives the agent's session updates. - */ - client: Client | ((agent: Agent) => Client) -} +export { DEFAULT_STOP_TIMEOUT_MS } from './transport' +export type { AcpAgentExit, AcpStartOpts, AcpStopOpts } from './transport' /** * ACP agent running in a sandbox, connected to your client. @@ -64,13 +50,15 @@ export class AcpAgent { } /** - * Close the agent: send EOF to its stdin and kill it if it has not exited + * Stop the agent: send EOF to its stdin and kill it if it has not exited * within `timeoutMs` of this call. Messages the agent sends after this call - * are not delivered. It rejects if the agent could not be killed. + * are not delivered. * - * @param opts options for closing the agent. + * @param opts options for stopping the agent. + * + * @throws SandboxError if the agent could not be killed. */ - async close(opts?: AcpCloseOpts): Promise { + async stop(opts?: AcpStopOpts): Promise { return this.transport.close(opts) } @@ -95,10 +83,18 @@ export class AcpAgent { * servers you pass to the agent are not restricted. * * @param sandbox sandbox with the agent installed. - * @param opts options for starting and connecting to the agent. + * @param command command that starts the agent in ACP mode, for example `goose acp`. + * @param client your ACP client, or a function that creates it for the agent + * connection. It answers permission requests and receives the agent's session updates. + * @param opts options for starting the agent. * * @returns `AcpAgent` with the ACP connection. * + * @throws InvalidArgumentError if `maxMessageBytes` is not a positive integer. + * @throws SandboxError if the agent could not be started. Later, the + * connection fails with a `SandboxError` if the agent exits with a non-zero + * code or sends a message over `maxMessageBytes`. + * * @example * ```ts * import { Sandbox } from 'e2b' @@ -106,18 +102,19 @@ export class AcpAgent { * import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk' * * const sandbox = await Sandbox.create('goose') - * const agent = await connectAcpAgent(sandbox, { command: 'goose acp', client }) + * const agent = await connectAcpAgent(sandbox, 'goose acp', client) * await agent.connection.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} }) * // ... - * await agent.close() + * await agent.stop() * ``` */ export async function connectAcpAgent( sandbox: Sandbox, - opts: ConnectAcpAgentOpts + command: string, + client: Client | ((agent: Agent) => Client), + opts?: AcpStartOpts ): Promise { - const { command, client, ...startOpts } = opts - const transport = await startAcpTransport(sandbox, command, startOpts) + const transport = await startAcpTransport(sandbox, command, opts) const toClient = typeof client === 'function' ? client : () => client let connection: ClientSideConnection try { diff --git a/packages/acp-js/src/transport.ts b/packages/acp-js/src/transport.ts index 2ae3625132..256f885eed 100644 --- a/packages/acp-js/src/transport.ts +++ b/packages/acp-js/src/transport.ts @@ -19,7 +19,7 @@ import { */ export interface AcpStartOpts extends Pick< CommandStartOpts, - 'cwd' | 'envs' | 'user' | 'requestTimeoutMs' + 'cwd' | 'envs' | 'user' | 'requestTimeoutMs' | 'signal' > { /** * Callback for the agent's stderr output (logs). The agent's stdout is reserved for the protocol. @@ -34,14 +34,19 @@ export interface AcpStartOpts extends Pick< } /** - * Options for closing an ACP agent. + * Default time for an ACP agent to exit after `stop()`, in **milliseconds**. */ -export interface AcpCloseOpts { +export const DEFAULT_STOP_TIMEOUT_MS = 5_000 + +/** + * Options for stopping an ACP agent. + */ +export interface AcpStopOpts { /** - * Time for the agent to exit after `close()` is called, in **milliseconds**. + * Time for the agent to exit after `stop()` is called, in **milliseconds**. * It covers sending EOF to the agent's stdin; after it the agent is killed. * - * @default 5_000 // 5 seconds + * @default DEFAULT_STOP_TIMEOUT_MS // 5 seconds */ timeoutMs?: number } @@ -110,7 +115,7 @@ export class AcpTransport { * * It rejects if the agent could not be killed. */ - async close(opts?: AcpCloseOpts): Promise { + async close(opts?: AcpStopOpts): Promise { // Only a known exit status means the process is gone. A failed output // stream can leave it running. if (this.exitStatus()) { @@ -127,7 +132,10 @@ export class AcpTransport { () => false ), new Promise((resolve) => { - timer = setTimeout(() => resolve(false), opts?.timeoutMs ?? 5_000) + timer = setTimeout( + () => resolve(false), + opts?.timeoutMs ?? DEFAULT_STOP_TIMEOUT_MS + ) }), ]) clearTimeout(timer) @@ -290,6 +298,7 @@ export async function startAcpTransport( envs: opts?.envs, user: opts?.user, requestTimeoutMs: opts?.requestTimeoutMs, + signal: opts?.signal, onStdout: async (data) => { if (ended || closing) { return diff --git a/packages/acp-js/tests/connect.test.ts b/packages/acp-js/tests/connect.test.ts index 60b11c513e..20b6be3abe 100644 --- a/packages/acp-js/tests/connect.test.ts +++ b/packages/acp-js/tests/connect.test.ts @@ -13,10 +13,7 @@ const client: Client = { sandboxTest('connectAcpAgent connects an ACP client', async ({ sandbox }) => { await uploadFakeAgent(sandbox) - const agent = await connectAcpAgent(sandbox, { - command: FAKE_AGENT_CMD, - client, - }) + const agent = await connectAcpAgent(sandbox, FAKE_AGENT_CMD, client) const response = await agent.connection.initialize({ protocolVersion: PROTOCOL_VERSION, @@ -24,6 +21,6 @@ sandboxTest('connectAcpAgent connects an ACP client', async ({ sandbox }) => { }) assert.equal(response.protocolVersion, 1) - await agent.close() + await agent.stop() assert.equal((await agent.wait()).exitCode, 0) }) diff --git a/packages/acp-js/tests/transport.offline.test.ts b/packages/acp-js/tests/transport.offline.test.ts index f27f417efd..07c4a2cf51 100644 --- a/packages/acp-js/tests/transport.offline.test.ts +++ b/packages/acp-js/tests/transport.offline.test.ts @@ -192,11 +192,8 @@ test('connect kills the agent when the client factory throws', async () => { const agent = fakeAgent() await expect( - connectAcpAgent(agent.sandbox as Sandbox, { - command: 'agent', - client: () => { - throw new Error('no client') - }, + connectAcpAgent(agent.sandbox as Sandbox, 'agent', () => { + throw new Error('no client') }) ).rejects.toThrow('no client') expect(agent.handle.kill).toHaveBeenCalledOnce() From 6c1a1a41629da51dec89ec72bda6b2d84bce8473 Mon Sep 17 00:00:00 2001 From: Maksim Obukhov Date: Mon, 19 Oct 2026 10:05:04 +0200 Subject: [PATCH 26/26] refactor(acp-python): make options keyword-only, rename close() to stop() connect_acp_agent() takes its optional settings as keyword arguments only. stop() ends the agent process that connect_acp_agent() started, and its default timeout is the exported DEFAULT_STOP_TIMEOUT. --- packages/acp-python/README.md | 4 ++-- packages/acp-python/e2b_acp/__init__.py | 19 ++++++++++++------- packages/acp-python/e2b_acp/transport.py | 7 ++++++- packages/acp-python/tests/test_connect.py | 2 +- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/acp-python/README.md b/packages/acp-python/README.md index a6ce9317c8..f484a5687b 100644 --- a/packages/acp-python/README.md +++ b/packages/acp-python/README.md @@ -57,7 +57,7 @@ async def main(): session_id=session.session_id, prompt=[acp.text_block("Create fizzbuzz.py for 1 to 15 and run it.")], ) - await agent.close() + await agent.stop() finally: await sandbox.kill() @@ -71,7 +71,7 @@ The agent talks ACP over its stdin and stdout. `connect_acp_agent` turns off the ## Lifecycle -- `agent.close()` sends EOF to the agent and kills it if it has not exited within `timeout` (5 seconds by default) of the call. Messages the agent sends after that are not delivered. +- `agent.stop()` sends EOF to the agent and kills it if it has not exited within `timeout` (5 seconds by default) of the call. Messages the agent sends after that are not delivered. - `agent.wait()` returns the exit code once the agent exits. - If the agent exits, pending requests on the connection fail with a connection error. Use `agent.wait()` for the exit code and pass `on_stderr` to receive the agent's logs. - The agent runs as long as the sandbox does, so set the sandbox timeout for the longest task you expect. diff --git a/packages/acp-python/e2b_acp/__init__.py b/packages/acp-python/e2b_acp/__init__.py index 924af01205..7d96fbff8e 100644 --- a/packages/acp-python/e2b_acp/__init__.py +++ b/packages/acp-python/e2b_acp/__init__.py @@ -8,7 +8,7 @@ from e2b import AsyncSandbox from e2b.connection_config import Username -from .transport import AcpAgentExit, AcpTransport, StderrHandler +from .transport import DEFAULT_STOP_TIMEOUT, AcpAgentExit, AcpTransport, StderrHandler from .utils import DEFAULT_MAX_MESSAGE_BYTES @@ -45,14 +45,15 @@ async def wait(self) -> AcpAgentExit: """ return await self._transport.wait() - async def close(self, timeout: float = 5) -> None: + async def stop(self, *, timeout: float = DEFAULT_STOP_TIMEOUT) -> None: """ - Close the agent: send EOF to its stdin and kill it if it has not + Stop the agent: send EOF to its stdin and kill it if it has not exited within `timeout` of this call. Messages the agent sends after - this call are not delivered. It raises if the agent could not be - killed. + this call are not delivered. :param timeout: Time for the agent to exit in **seconds** + + :raises SandboxException: If the agent could not be killed """ await self._transport.close(timeout) @@ -69,6 +70,7 @@ async def connect_acp_agent( sandbox: AsyncSandbox, command: str, client: acp.Client, + *, cwd: Optional[str] = None, envs: Optional[Dict[str, str]] = None, user: Optional[Username] = None, @@ -95,7 +97,7 @@ async def connect_acp_agent( agent = await connect_acp_agent(sandbox, "goose acp", client) await agent.connection.initialize(protocol_version=acp.PROTOCOL_VERSION) # ... - await agent.close() + await agent.stop() ``` :param sandbox: Sandbox with the agent installed @@ -109,10 +111,13 @@ async def connect_acp_agent( :param request_timeout: Timeout for starting the agent in **seconds** :return: `AcpAgent` with the ACP connection + + :raises InvalidArgumentException: If `max_message_bytes` is not a positive integer + :raises SandboxException: If the agent could not be started """ transport = AcpTransport(sandbox, max_message_bytes, on_stderr) await transport.start(command, envs, user, cwd, request_timeout) return AcpAgent(transport, acp.connect_to_agent(client, transport)) -__all__ = ["AcpAgent", "AcpAgentExit", "connect_acp_agent"] +__all__ = ["DEFAULT_STOP_TIMEOUT", "AcpAgent", "AcpAgentExit", "connect_acp_agent"] diff --git a/packages/acp-python/e2b_acp/transport.py b/packages/acp-python/e2b_acp/transport.py index 5f5ec349d0..67ff97cb88 100644 --- a/packages/acp-python/e2b_acp/transport.py +++ b/packages/acp-python/e2b_acp/transport.py @@ -16,6 +16,11 @@ restrict_client_capabilities, ) +DEFAULT_STOP_TIMEOUT = 5 +""" +Default time for an ACP agent to exit after `stop()`, in **seconds**. +""" + STDERR_TAIL_CHARS = 4096 READ_QUEUE_MESSAGES = 64 @@ -101,7 +106,7 @@ async def receive(self) -> Optional[JsonRpcMessage]: self._readable.clear() await self._readable.wait() - async def close(self, timeout: float = 5) -> None: + async def close(self, timeout: float = DEFAULT_STOP_TIMEOUT) -> None: # Only a known exit status means the process is gone. A failed output # stream can leave it running. if self._exit is not None or self._exit_task is None: diff --git a/packages/acp-python/tests/test_connect.py b/packages/acp-python/tests/test_connect.py index 1428dfb0c0..c8843e8646 100644 --- a/packages/acp-python/tests/test_connect.py +++ b/packages/acp-python/tests/test_connect.py @@ -26,5 +26,5 @@ async def test_connect_acp_agent_connects_an_acp_client(async_sandbox: AsyncSand ) assert response.protocol_version == 1 - await agent.close() + await agent.stop() assert (await agent.wait()).exit_code == 0