diff --git a/.agents/skills/acpkit-sdk/SKILL.md b/.agents/skills/acpkit-sdk/SKILL.md index d5c80de..a3d0e0b 100644 --- a/.agents/skills/acpkit-sdk/SKILL.md +++ b/.agents/skills/acpkit-sdk/SKILL.md @@ -67,6 +67,7 @@ What it owns: What it does not own: - Pydantic plan/approval/projection details +- Pydantic prompt capability, custom slash command, and external hook event details - LangChain graph/provider/projection details - WebSocket transport behavior - Codex auth parsing @@ -258,7 +259,7 @@ Remote pairing examples: Switch to a narrower skill when: - the bug is clearly inside adapter runtime behavior -- the task is about approvals, plans, projections, or host policy +- the task is about approvals, plans, prompt capabilities, custom slash commands, projections, external hook events, or host policy - the task is about transport or remote ownership rather than CLI or dispatch - the task is about Codex auth refresh or `auth.json` @@ -275,5 +276,6 @@ Stay in this skill when: - Do not describe `acpremote` as an adapter. - Do not describe `codex-auth-helper` as part of target resolution. - Do not document a root CLI feature that is not present in the [CLI module](https://github.com/vcoderun/acpkit/blob/main/src/acpkit/cli.py). -- When the question is about adapter truthfulness, plans, approvals, projections, host ownership, - or provider behavior, move to the narrower package skill. +- When the question is about adapter truthfulness, plans, approvals, prompt capabilities, custom + slash commands, projections, external hook events, host ownership, or provider behavior, move to + the narrower package skill. diff --git a/.agents/skills/acpkit-sdk/resources/intro.md b/.agents/skills/acpkit-sdk/resources/intro.md index a250fa9..c9e013e 100644 --- a/.agents/skills/acpkit-sdk/resources/intro.md +++ b/.agents/skills/acpkit-sdk/resources/intro.md @@ -2,9 +2,9 @@ ACP Kit is the adapter toolkit and monorepo for turning an existing agent surface into a truthful ACP server boundary. -Today the stable production focus is `pydantic-acp`: exposing `pydantic_ai.Agent` through ACP while keeping models, modes, plans, approvals, MCP metadata, host tools, and session state aligned with what the underlying runtime can actually support. - -Additional adapters such as `langchain-acp` and `dspy-acp` are planned after `pydantic-acp` reaches 1.0 stability. +Today the repo ships production-grade adapters for both `pydantic-acp` and `langchain-acp`. +`pydantic-acp` remains the richest reference implementation for ACP-native models, modes, plans, +approvals, MCP metadata, host tools, projection, and session state. This intro is intentionally short. The canonical deep references should come from the published docs set, not from a second parallel skill-specific spec. @@ -21,7 +21,8 @@ The central contract is: > expose ACP state only when the underlying runtime can actually honor it. -That rule drives model selection, mode switching, slash commands, native plan state, approval flow, MCP metadata, and host-backed tooling. +That rule drives model selection, mode switching, slash commands, prompt capabilities, native plan +state, approval flow, MCP metadata, external hook events, projection, and host-backed tooling. ## Start With The Real Docs @@ -32,13 +33,14 @@ Use these published docs pages as the primary references: | Product overview and package map | [ACP Kit Overview](https://vcoderun.github.io/acpkit/) | | Construction seams and adapter overview | [Pydantic ACP Overview](https://vcoderun.github.io/acpkit/pydantic-acp/) | | Runtime config and session ownership | [AdapterConfig](https://vcoderun.github.io/acpkit/pydantic-acp/adapter-config/) | -| Models, modes, slash commands, thinking | [Models, Modes, and Slash Commands](https://vcoderun.github.io/acpkit/pydantic-acp/runtime-controls/) | -| Plans, approvals, and cancellation | [Plans, Thinking, and Approvals](https://vcoderun.github.io/acpkit/pydantic-acp/plans-thinking-approvals/) | +| Models, modes, custom slash commands, thinking | [Models, Modes, and Slash Commands](https://vcoderun.github.io/acpkit/pydantic-acp/runtime-controls/) | +| Plans, approvals, permission presentation, and cancellation | [Plans, Thinking, and Approvals](https://vcoderun.github.io/acpkit/pydantic-acp/plans-thinking-approvals/) | | Host-owned state patterns | [Providers](https://vcoderun.github.io/acpkit/providers/) | -| ACP-visible extension seams | [Bridges](https://vcoderun.github.io/acpkit/bridges/) | -| Host-backed tools and projections | [Host Backends and Projections](https://vcoderun.github.io/acpkit/host-backends/) | +| ACP-visible extension seams and external hook events | [Bridges](https://vcoderun.github.io/acpkit/bridges/) | +| Host-backed tools, search/list projection, and classification | [Host Backends and Projections](https://vcoderun.github.io/acpkit/host-backends/) | | Maintained example ladder | [Examples Overview](https://vcoderun.github.io/acpkit/examples/) | -| Production showcase | [Workspace Agent](https://vcoderun.github.io/acpkit/examples/workspace-agent/) | +| Pydantic production showcase | [Finance Agent](https://vcoderun.github.io/acpkit/examples/finance/) | +| LangChain production showcase | [LangChain Workspace Graph](https://vcoderun.github.io/acpkit/examples/langchain-workspace/) | | API surface | [pydantic_acp API](https://vcoderun.github.io/acpkit/api/pydantic_acp/) | ## Construction Seams To Reach For @@ -61,19 +63,80 @@ Use these seams intentionally: - `FileSessionStore` is the hardened local durable store: atomic replace writes, local locking, malformed-session tolerance, and stale temp cleanup; it is not a distributed multi-writer backend - slash mode commands are dynamic; `ask`, `plan`, and `agent` are examples, not built-in global names - mode ids must not collide with reserved slash command names like `model`, `thinking`, `tools`, `hooks`, or `mcp-servers` +- custom slash commands come from `SlashCommandProvider` or `StaticSlashCommandProvider`, and + must not collide with built-in commands or mode names +- `AdapterConfig.prompt_capabilities` controls what prompt input families are advertised; do not + advertise image, audio, or embedded context unless the runtime can honor them - only one `PrepareToolsMode(..., plan_mode=True)` is allowed - `plan_tools=True` is how a non-plan execution mode keeps plan progress tools visible +- `PrepareOutputToolsBridge` is the separate seam for structured-output tool preparation in + current Pydantic AI +- `HookBridge` covers output-tool preparation, output validation, output processing, and + deferred tool-call observation - `/thinking` only exists when `ThinkingBridge()` is configured - native ACP plan state and `PlanProvider` are separate ownership paths +- permission card rendering is `NativeApprovalBridge.tool_call_builder`, not an `AdapterConfig` + field +- `ApprovalBridge` stays compatible with the legacy no-`projection_map` signature; use + `ProjectionAwareApprovalBridge` only when the bridge explicitly accepts projected context +- remembered approval policy is live runtime state owned by `ApprovalPolicyStore`, while + `ApprovalStateProvider` is metadata-only - `HookBridge(hide_all=True)` suppresses hook listing output, not the underlying hook capability itself +- `ExternalHookEventBridge` is for integrations that already own lifecycle events and want them + buffered into ACP updates - custom `run_event_stream` hooks and wrappers must return an async iterable, not a coroutine - `HostAccessPolicy` is the native typed guardrail surface for host-backed file and terminal access +- `FileSystemProjectionMap` search/list tree rendering is opt-in and based only on tool output; + it must not read the filesystem +- `ProjectionAwareToolClassifier` classifies only configured projection tool names and delegates + unknown tools to the base classifier - `BlackBoxHarness` is the reusable black-box ACP boundary test helper for downstream integrations - projection helper primitives handle diff previews, truncation, command summaries, and guardrail-aware caution text without each integration rebuilding that shaping logic - the compatibility manifest schema gives integrations one typed, reviewable declaration of which ACP surfaces are implemented, partial, intentionally not used, or planned ## New Native Surfaces +### PromptCapabilities, SlashCommandProvider, And StaticSlashCommandProvider + +Reach for `AdapterConfig.prompt_capabilities` when the runtime's prompt input support differs from +the defaults. Use `SlashCommandProvider` or `StaticSlashCommandProvider` when a product integration +needs commands beyond the built-in model, mode, config, thinking, tools, hooks, and MCP surfaces. + +Custom command handlers can return transcript updates, text, or a handled/fallthrough decision. The +default result refreshes the session surface so visible commands and related state stay synchronized. + +Read next: + +- https://vcoderun.github.io/acpkit/pydantic-acp/runtime-controls/ +- https://vcoderun.github.io/acpkit/pydantic-acp/adapter-config/ + +### ApprovalPolicyStore And PermissionToolCallBuilder + +Reach for `ApprovalPolicyStore` when remembered approval policy needs to live somewhere other than +session metadata. Reach for `PermissionToolCallBuilder` when native approvals should render custom +permission cards while keeping the ACP approval lifecycle unchanged. + +Keep the builder on `NativeApprovalBridge.tool_call_builder`. If a custom approval bridge needs +projected file or command context, implement `ProjectionAwareApprovalBridge` rather than widening the +legacy `ApprovalBridge` protocol. + +Read next: + +- https://vcoderun.github.io/acpkit/pydantic-acp/plans-thinking-approvals/ +- https://vcoderun.github.io/acpkit/providers/ + +### ExternalHookEventBridge And ProjectionAwareToolClassifier + +Reach for `ExternalHookEventBridge` when hook-like lifecycle events come from an external runtime and +should appear through the normal buffered bridge update path. Reach for +`ProjectionAwareToolClassifier` when configured projection maps should also drive ACP tool-kind +classification for reads, writes, bash, and search tools. + +Read next: + +- https://vcoderun.github.io/acpkit/bridges/ +- https://vcoderun.github.io/acpkit/host-backends/ + ### HostAccessPolicy Reach for `HostAccessPolicy` when an integration needs one reusable, typed place to evaluate file and command risk. @@ -162,10 +225,13 @@ Read next: ## Reference Files In This Skill -These skill-local references are only routing aids back into the docs: +These skill-local references are only routing aids back into the docs and source tree: -- `references/package-surface.md` -- `references/runtime-capabilities.md` -- `references/docs-examples-map.md` +- `SKILL.md` +- `resources/intro.md` +- `examples/README.md` +- `scripts/list_examples.py` +- `scripts/list_public_exports.py` -Use them to find the right docs page quickly, not as independent source-of-truth specs. +Use them to find the right docs page or package surface quickly, not as independent source-of-truth +specs. diff --git a/.agents/skills/codex-auth-helper/SKILL.md b/.agents/skills/codex-auth-helper/SKILL.md index 57a5ece..aff7a9e 100644 --- a/.agents/skills/codex-auth-helper/SKILL.md +++ b/.agents/skills/codex-auth-helper/SKILL.md @@ -155,7 +155,7 @@ This helper is usually upstream of the adapter, not a replacement for it. ### Build a Codex-backed `pydantic-ai` model -Use `create_codex_responses_model(...)`. +Use `create_codex_responses_model(...)` and pass explicit `instructions=...`. ### Build a lower-level client first @@ -164,7 +164,8 @@ Use `create_codex_async_openai(...)` when you need the transport/client object e ### Build a LangChain model Use `create_codex_chat_openai(...)` when the upstream runtime is LangChain or LangGraph and you -want the Responses API path instead of hand-wiring `langchain-openai`. +want the Responses API path instead of hand-wiring `langchain-openai`. Pass explicit +`instructions=...` here too. ### Debug refresh behavior diff --git a/.agents/skills/codex-auth-helper/examples/codex_chat_openai_graph.py b/.agents/skills/codex-auth-helper/examples/codex_chat_openai_graph.py index 9a70381..9e88804 100644 --- a/.agents/skills/codex-auth-helper/examples/codex_chat_openai_graph.py +++ b/.agents/skills/codex-auth-helper/examples/codex_chat_openai_graph.py @@ -19,7 +19,13 @@ def describe_codex_surface() -> str: graph = create_agent( - model=create_codex_chat_openai(MODEL_NAME), + model=create_codex_chat_openai( + MODEL_NAME, + instructions=( + "You are a helpful coding assistant. " + "Explain concrete workspace observations and use tools when helpful." + ), + ), tools=[describe_codex_surface], name="codex-chat-openai-graph", ) diff --git a/.agents/skills/codex-auth-helper/examples/codex_responses_agent.py b/.agents/skills/codex-auth-helper/examples/codex_responses_agent.py index 103ee9a..6589932 100644 --- a/.agents/skills/codex-auth-helper/examples/codex_responses_agent.py +++ b/.agents/skills/codex-auth-helper/examples/codex_responses_agent.py @@ -9,11 +9,14 @@ MODEL_NAME = os.getenv("CODEX_MODEL", "gpt-5.4") agent = Agent( - create_codex_responses_model(MODEL_NAME), - name="codex-responses-agent", - instructions=( - "You are a concise coding assistant. Ask for clarification when the task is underspecified." + create_codex_responses_model( + MODEL_NAME, + instructions=( + "You are a concise coding assistant. " + "Ask for clarification when the task is underspecified." + ), ), + name="codex-responses-agent", ) diff --git a/.agents/skills/pydantic-acp/SKILL.md b/.agents/skills/pydantic-acp/SKILL.md index 39cf31f..bc3d844 100644 --- a/.agents/skills/pydantic-acp/SKILL.md +++ b/.agents/skills/pydantic-acp/SKILL.md @@ -1,6 +1,6 @@ --- name: "pydantic-acp" -description: "Use for `pydantic-acp` tasks: exposing `pydantic_ai.Agent` through ACP, adapter config/runtime ownership, approvals, plans, hooks, projections, host-backed tools, and Pydantic-specific examples." +description: "Use for `pydantic-acp` tasks: exposing `pydantic_ai.Agent` through ACP, adapter config/runtime ownership, prompt capabilities, slash commands, approvals, plans, hooks, projections, host-backed tools, and Pydantic-specific examples." --- # pydantic-acp Skill @@ -16,11 +16,14 @@ In this package that rule affects: - model selection - mode switching - config options +- prompt capability advertisement - ACP-native plans - approval flows - host-backed files and terminal access - tool projection - hook visibility +- external hook event projection +- custom slash commands - session replay ## Start Here @@ -30,18 +33,20 @@ If you only need the shortest high-signal path: 1. read `Quick Routing` 2. open the [adapter config module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/config.py) and the [package entrypoint](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/__init__.py) for public-surface questions 3. open the [runtime adapter](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/adapter.py) for lifecycle and dispatch questions -4. then branch into approvals, projections, host, or plans +4. then branch into approvals, projections, host, plans, slash commands, or prompt capabilities ## Quick Routing | If the task is about... | Use this skill? | Open first | | --- | --- | --- | | `run_acp(agent=...)` or `create_acp_agent(...)` | Yes | [package entrypoint](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/__init__.py), [adapter config module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/config.py), [runtime adapter](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/adapter.py) | -| approvals or remembered policy | Yes | [approvals module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/approvals.py), [prompt-execution runtime](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_prompt_execution.py) | +| approvals, permission presentation, or remembered policy | Yes | [approvals module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/approvals.py), [approval store module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/approval_store.py), [permission presentation module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/permission_presentation.py), [prompt-execution runtime](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_prompt_execution.py) | | plans or plan generation | Yes | [prepare-tools bridge](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/prepare_tools.py), [native plan runtime](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_native_plan_runtime.py), [models module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/models.py) | | filesystem / terminal ownership | Yes | [host context module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/host/context.py), [filesystem host backend](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/host/filesystem.py), [terminal host backend](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/host/terminal.py), [host policy module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/host/policy.py) | -| hook visibility or hook projection | Yes | [hooks bridge](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/hooks.py), [hook-introspection runtime](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/hook_introspection.py), [hook projection module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/hook_projection.py) | -| slash commands / model / mode surface | Yes | [slash-commands runtime](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/slash_commands.py), [providers module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/providers.py), [models module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/models.py) | +| hook visibility or external hook projection | Yes | [hooks bridge](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/hooks.py), [external hooks bridge](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/external_hooks.py), [hook-introspection runtime](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/hook_introspection.py), [hook projection module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/hook_projection.py) | +| slash commands / model / mode surface | Yes | [custom slash command module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/slash.py), [slash-commands runtime](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/slash_commands.py), [adapter-prompt runtime](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_adapter_prompt.py), [providers module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/providers.py) | +| prompt capabilities or multimodal input flags | Yes | [prompt capabilities module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/prompt_capabilities.py), [adapter config module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/config.py), [prompt/resources docs](https://github.com/vcoderun/acpkit/blob/main/docs/pydantic-acp/prompt-resources.md) | +| filesystem search/list projection or tool classification | Yes | [projection module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/projection.py), [host backends docs](https://github.com/vcoderun/acpkit/blob/main/docs/host-backends.md), [projection cookbook](https://github.com/vcoderun/acpkit/blob/main/docs/projection-cookbook.md) | | Codex auth refresh or `auth.json` | No, pair with `codex-auth-helper` | [Codex helper package](https://github.com/vcoderun/acpkit/tree/main/packages/helpers/codex-auth-helper) | | remote hosting or WebSocket transport | No, pair with `acpremote` | [remote transport package](https://github.com/vcoderun/acpkit/tree/main/packages/transports/acpremote) | @@ -56,6 +61,8 @@ It owns: - ACP-native plan state and plan updates - approval lifecycle and remembered approval policies - hook introspection and hook projection +- external hook event buffering +- custom slash command discovery and handling - host-backed filesystem and terminal ownership - tool projection maps - session store semantics and transcript replay @@ -83,7 +90,10 @@ Package references: - [Raw overview docs](https://raw.githubusercontent.com/vcoderun/acpkit/main/docs/pydantic-acp.md) - [Raw host backends docs](https://raw.githubusercontent.com/vcoderun/acpkit/main/docs/host-backends.md) - [Raw projection cookbook](https://raw.githubusercontent.com/vcoderun/acpkit/main/docs/projection-cookbook.md) +- [Raw runtime controls docs](https://raw.githubusercontent.com/vcoderun/acpkit/main/docs/pydantic-acp/runtime-controls.md) +- [Raw plans, thinking, and approvals docs](https://raw.githubusercontent.com/vcoderun/acpkit/main/docs/pydantic-acp/plans-thinking-approvals.md) - [Raw prompt/resources docs](https://raw.githubusercontent.com/vcoderun/acpkit/main/docs/pydantic-acp/prompt-resources.md) +- [Raw API docs](https://raw.githubusercontent.com/vcoderun/acpkit/main/docs/api/pydantic_acp.md) - [Rendered overview](https://vcoderun.github.io/acpkit/pydantic-acp/) - [Source tree](https://github.com/vcoderun/acpkit/tree/main/packages/adapters/pydantic-acp) @@ -102,7 +112,20 @@ High-value public seams: - `AdapterConfig(...)` - `MemorySessionStore` - `FileSessionStore` +- `AdapterPromptCapabilities` - `NativeApprovalBridge` +- `PermissionToolCallBuilder` +- `ApprovalPolicyStore` +- `PrepareToolsBridge` +- `PrepareToolsMode` +- `PrepareOutputToolsBridge` +- `PrepareOutputToolsMode` +- `ThinkingBridge` +- `HookBridge` +- `SlashCommandProvider` +- `StaticSlashCommandProvider` +- `ExternalHookEventBridge` +- `ProjectionAwareToolClassifier` - `ClientHostContext` - `CompatibilityManifest` - `BlackBoxHarness` @@ -111,13 +134,26 @@ Package entrypoint: - [Package entrypoint](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/__init__.py) +## Current Pydantic AI Compatibility + +`pydantic-acp` currently targets `pydantic-ai-slim==1.92.0`. + +When working on this surface, remember: + +- `PrepareToolsBridge` owns function-tool preparation and mode-specific plan tool visibility +- `PrepareOutputToolsBridge` owns structured-output tool preparation and session metadata for output-tool modes +- `HookBridge` covers output-tool preparation, output validation, output processing, and deferred tool-call observation +- prompt runtime passes ACP session identity through Pydantic AI `conversation_id` and run `metadata` +- `run_stream_events()` returns an async context manager in current Pydantic AI; keep direct async-iterable fallback only for tests and compatibility fakes +- `OpenAICompactionBridge` must not pass deprecated `instructions=` into upstream `OpenAICompaction` + ## Module Guide | Subsystem | Key files | Use them for | | --- | --- | --- | -| public surface and construction | [package entrypoint](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/__init__.py), [adapter config module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/config.py), [agent source module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/agent_source.py), [agent type definitions](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/agent_types.py), [models module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/models.py), [providers module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/providers.py) | public API shape, construction seams, provider contracts | -| approvals | [approvals module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/approvals.py), [prompt-execution runtime](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_prompt_execution.py), [prompt runtime](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_prompt_runtime.py) | deferred approvals, remembered policy, permission flow | -| bridges | [base bridge module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/base.py), [capability-support bridge](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/capability_support.py), [history-processor bridge](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/history_processor.py), [hooks bridge](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/hooks.py), [MCP bridge](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/mcp.py), [prepare-tools bridge](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/prepare_tools.py), [thinking bridge](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/thinking.py) | optional capability wiring and extension seams | +| public surface and construction | [package entrypoint](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/__init__.py), [adapter config module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/config.py), [prompt capabilities module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/prompt_capabilities.py), [agent source module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/agent_source.py), [agent type definitions](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/agent_types.py), [models module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/models.py), [providers module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/providers.py) | public API shape, construction seams, prompt capability flags, provider contracts | +| approvals | [approvals module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/approvals.py), [approval store module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/approval_store.py), [permission presentation module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/permission_presentation.py), [prompt-execution runtime](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_prompt_execution.py), [prompt runtime](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_prompt_runtime.py) | deferred approvals, remembered policy, permission cards, projection-aware approval context | +| bridges | [base bridge module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/base.py), [capability-support bridge](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/capability_support.py), [external hooks bridge](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/external_hooks.py), [history-processor bridge](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/history_processor.py), [hooks bridge](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/hooks.py), [MCP bridge](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/mcp.py), [prepare-tools bridge](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/prepare_tools.py), [thinking bridge](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/thinking.py) | optional capability wiring, external event projection, and extension seams | | projection | [projection module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/projection.py), [projection helper module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/projection_helpers.py), [projection text helpers](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/_projection_text.py), [projection risk helpers](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/_projection_risk.py), [hook projection module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/hook_projection.py) | ACP-visible transcript cards and rendering | | host ownership | [host context module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/host/context.py), [filesystem host backend](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/host/filesystem.py), [terminal host backend](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/host/terminal.py), [host policy module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/host/policy.py), [path policy helpers](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/host/_policy_paths.py), [command policy helpers](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/host/_policy_commands.py) | path safety, command safety, client-backed host behavior | | runtime core | [runtime adapter](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/adapter.py), [runtime server](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/server.py), [bridge manager](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/bridge_manager.py), [hook-introspection runtime](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/hook_introspection.py), [session surface module](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/session_surface.py), [slash-commands runtime](https://github.com/vcoderun/acpkit/blob/main/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/slash_commands.py) | adapter lifecycle, runtime update emission, slash command behavior | @@ -173,7 +209,8 @@ It supports: - mode switching - config options - ACP-native plans -- tool-based or structured plan generation +- `Tool Plans` or `Structured Plans` plan generation +- custom slash command providers - session replay and fork/resume/load/close/list lifecycle - slash command discovery and rendering @@ -187,6 +224,10 @@ This package should be the reference answer whenever the question is: High-value bridges include: +- `PrepareToolsBridge` +- `PrepareOutputToolsBridge` +- `ThinkingBridge` +- `HookBridge` - `ThreadExecutorBridge` - `SetToolMetadataBridge` - `IncludeToolReturnSchemasBridge` @@ -196,6 +237,7 @@ High-value bridges include: - `McpCapabilityBridge` - `ToolsetBridge` - `PrefixToolsBridge` +- `ExternalHookEventBridge` - `OpenAICompactionBridge` - `AnthropicCompactionBridge` @@ -206,11 +248,14 @@ High-value projection families include: - `BuiltinToolProjectionMap` - `HookProjectionMap` - `CompositeProjectionMap` +- `ProjectionAwareToolClassifier` Important rule: - bridges affect runtime behavior and metadata - projection maps affect ACP-visible transcript rendering +- `FileSystemProjectionMap` search/list tree rendering is opt-in and never reads the filesystem +- `ProjectionAwareToolClassifier` classifies only configured tool names and delegates unknown tools Split those concerns before editing. @@ -238,6 +283,7 @@ Use this skill when the task is about: This package also owns the more subtle Pydantic-specific surfaces: - prompt-to-input conversion +- prompt capability advertisement through `AdapterPromptCapabilities` - prompt-model override providers - media-aware model routing - transcript-to-history rebuilding @@ -330,4 +376,11 @@ Stay in this skill when the main issue is: - Do not route LangChain or DeepAgents questions through this skill. - Do not answer Codex auth refresh questions from here unless the adapter integration itself is the point. +- Do not add `permission_tool_call_builder` to `AdapterConfig`; permission rendering belongs on + `NativeApprovalBridge.tool_call_builder`. +- Keep `ApprovalBridge` compatible with the legacy no-`projection_map` signature. Use + `ProjectionAwareApprovalBridge` only for bridges that explicitly accept projected context. +- Do not make custom slash commands collide with built-in commands or mode names. +- Treat `ExternalHookEventBridge.metadata_key=None` as the way to suppress bridge metadata + publication. - If the task is really about remote transport, move to `acpremote`. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70fb8ef..03b6dcd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: enable-cache: true - name: Install Dependencies - run: uv sync --all-extras + run: uv sync --frozen --all-extras - name: Check Formatting run: uv run ruff format --check @@ -44,7 +44,7 @@ jobs: - name: Run Tests run: make tests - - name: Build Root, Adapter, And Helper Projects + - name: Build Root, Adapter, Helper, And Transport Projects shell: bash run: | project_dirs() { @@ -54,6 +54,9 @@ jobs: if [ -d packages/adapters ]; then find packages/adapters -type f -name pyproject.toml -exec dirname {} \; | sort -u fi + if [ -d packages/transports ]; then + find packages/transports -type f -name pyproject.toml -exec dirname {} \; | sort -u + fi printf '.\n' } @@ -62,9 +65,9 @@ jobs: local pyproject_path="$project_dir/pyproject.toml" if grep -Eq '^\[project\.optional-dependencies\]' "$pyproject_path"; then - uv sync --project "$project_dir" --all-extras + uv sync --project "$project_dir" --frozen --all-extras else - uv sync --project "$project_dir" + uv sync --project "$project_dir" --frozen fi } diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 7c2e9d1..f7e3308 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,7 +2,7 @@ name: Docs on: push: - branches: [ "main"] + branches: ["main", "acpkit_v1"] workflow_dispatch: permissions: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9d8daa6..3a1ba71 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,6 +8,7 @@ on: permissions: contents: read + id-token: write concurrency: group: publish-${{ github.workflow }}-${{ github.ref }} @@ -17,8 +18,6 @@ jobs: publish: name: Build And Publish Workspace runs-on: ubuntu-latest - env: - UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }} steps: - name: Check Out Repository uses: actions/checkout@v6 @@ -35,10 +34,10 @@ jobs: - name: Validate Release run: | - uv sync --all-extras + uv sync --frozen --all-extras make prod - - name: Build And Publish Root, Adapter, And Helper Projects + - name: Build And Publish Root, Adapter, Helper, And Transport Projects shell: bash run: | project_dirs() { @@ -48,6 +47,9 @@ jobs: if [ -d packages/adapters ]; then find packages/adapters -type f -name pyproject.toml -exec dirname {} \; | sort -u fi + if [ -d packages/transports ]; then + find packages/transports -type f -name pyproject.toml -exec dirname {} \; | sort -u + fi printf '.\n' } @@ -56,9 +58,9 @@ jobs: local pyproject_path="$project_dir/pyproject.toml" if grep -Eq '^\[project\.optional-dependencies\]' "$pyproject_path"; then - uv sync --project "$project_dir" --all-extras + uv sync --project "$project_dir" --frozen --all-extras else - uv sync --project "$project_dir" + uv sync --project "$project_dir" --frozen fi } @@ -67,5 +69,5 @@ jobs: sync_project "$project_dir" rm -rf "$project_dir/dist" uv build --project "$project_dir" --out-dir "$project_dir/dist" - uv publish "$project_dir"/dist/* + uv publish --trusted-publishing always "$project_dir"/dist/* done < <(project_dirs) diff --git a/.gitignore b/.gitignore index b3d55cc..44fcca8 100644 --- a/.gitignore +++ b/.gitignore @@ -61,7 +61,6 @@ tmp/ # Cache & Config .pytest_cache/ .ruff_cache -uv.lock .logfire # Specs @@ -96,3 +95,6 @@ scripts/check_releases.py connect*.py expose*.py CHANGELOG +.acprouter-state +.deepagents-graph +.workspace-graph diff --git a/COVERAGE b/COVERAGE index 6945532..3e5acc6 100644 --- a/COVERAGE +++ b/COVERAGE @@ -1,2 +1,2 @@ -Line coverage: 100.00% (7734 / 7734) -Branch coverage: 100.00% (2552 / 2552) +Line coverage: 100.00% (8999 / 8999) +Branch coverage: 100.00% (3012 / 3012) diff --git a/README.md b/README.md index 8d9fa74..933654c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ACP Kit is the adapter toolkit and monorepo for exposing existing agent runtimes through ACP without inventing runtime features that are not really there. -Today the repo ships two production-grade adapter families: +Today the repo ships two maintained adapter families: - `pydantic-acp` - `langchain-acp` @@ -444,6 +444,7 @@ Top-level docs: - [LangChain ACP Overview](https://vcoderun.github.io/acpkit/langchain-acp/) - [Helpers](https://vcoderun.github.io/acpkit/helpers/) - [acpremote Overview](https://vcoderun.github.io/acpkit/acpremote/) +- [Security Guidance](https://vcoderun.github.io/acpkit/security/) - [AdapterConfig](https://vcoderun.github.io/acpkit/pydantic-acp/adapter-config/) - [Plans, Thinking, and Approvals](https://vcoderun.github.io/acpkit/pydantic-acp/plans-thinking-approvals/) - [Prompt Resources and Context](https://vcoderun.github.io/acpkit/pydantic-acp/prompt-resources/) diff --git a/VERSION b/VERSION index ac39a10..85b7c69 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.9.0 +0.9.6 diff --git a/docs/about/index.md b/docs/about/index.md index bea5d42..be3c9f0 100644 --- a/docs/about/index.md +++ b/docs/about/index.md @@ -25,9 +25,9 @@ Today it ships: The repository currently contains two adapter packages, a root CLI package, and two helper packages: - `pydantic-acp` - production-grade ACP adapter for `pydantic_ai.Agent` + maintained ACP adapter for `pydantic_ai.Agent` - `langchain-acp` - production-grade ACP adapter for LangChain, LangGraph, and DeepAgents graphs + maintained ACP adapter for LangChain, LangGraph, and DeepAgents graphs - `acpkit` root CLI, target resolver, and launch helpers - `codex-auth-helper` diff --git a/docs/api/pydantic_acp.md b/docs/api/pydantic_acp.md index 6e84d90..d2b8d97 100644 --- a/docs/api/pydantic_acp.md +++ b/docs/api/pydantic_acp.md @@ -16,6 +16,8 @@ This page documents the public surface re-exported by `pydantic_acp`. ::: pydantic_acp.AdapterModel +::: pydantic_acp.AdapterPromptCapabilities + ::: pydantic_acp.AcpSessionContext ::: pydantic_acp.JsonValue @@ -58,6 +60,14 @@ This page documents the public surface re-exported by `pydantic_acp`. ::: pydantic_acp.ApprovalStateProvider +::: pydantic_acp.ApprovalPolicy + +::: pydantic_acp.ApprovalPolicyStore + +::: pydantic_acp.SessionMetadataApprovalPolicyStore + +::: pydantic_acp.PermissionOptionSet + ## Bridge Classes ::: pydantic_acp.CapabilityBridge @@ -68,10 +78,18 @@ This page documents the public surface re-exported by `pydantic_acp`. ::: pydantic_acp.PrepareToolsMode +::: pydantic_acp.PrepareOutputToolsBridge + +::: pydantic_acp.PrepareOutputToolsMode + ::: pydantic_acp.ThinkingBridge ::: pydantic_acp.HookBridge +::: pydantic_acp.ExternalHookEventBridge + +::: pydantic_acp.EventEmissionMode + ::: pydantic_acp.HistoryProcessorBridge ::: pydantic_acp.ThreadExecutorBridge @@ -118,6 +136,36 @@ This page documents the public surface re-exported by `pydantic_acp`. ::: pydantic_acp.CompositeProjectionMap +::: pydantic_acp.ProjectionAwareToolClassifier + +## Approval Presentation + +::: pydantic_acp.PermissionRequestContext + +::: pydantic_acp.PermissionToolCallBuilder + +::: pydantic_acp.DefaultPermissionToolCallBuilder + +::: pydantic_acp.NativeApprovalBridge + +::: pydantic_acp.ProjectionAwareApprovalBridge + +::: pydantic_acp.supports_projection_aware_approval_bridge + +## Slash Commands + +::: pydantic_acp.SlashCommandRequest + +::: pydantic_acp.SlashCommandResult + +::: pydantic_acp.SlashCommandProvider + +::: pydantic_acp.StaticSlashCommand + +::: pydantic_acp.StaticSlashCommandProvider + +::: pydantic_acp.SlashCommandHandler + ## Projection Helpers ::: pydantic_acp.truncate_text diff --git a/docs/bridges.md b/docs/bridges.md index c38b66a..d055c64 100644 --- a/docs/bridges.md +++ b/docs/bridges.md @@ -37,6 +37,29 @@ Use plain `CapabilityBridge` when you only need to: Use `BufferedCapabilityBridge` when the bridge also needs to emit ACP transcript updates over time. +## External Hook Events + +Use `ExternalHookEventBridge` when an integration already knows about lifecycle events and wants to project them into ACP without installing Pydantic AI hooks or writing directly to the ACP client. + +```python +from pydantic_acp import ExternalHookEventBridge, HookEvent + +bridge = ExternalHookEventBridge() +bridge.record_event( + session, + HookEvent( + event_id="before_run", + hook_name="before_run", + tool_name=None, + tool_filters=(), + raw_output="completed", + status="completed", + ), +) +``` + +The bridge buffers updates and the adapter drains them through the normal bridge manager. Its session metadata appears under `external_hooks` by default and includes emission mode, pending event count, hidden event ids, and projection title prefix. + ### Override Matrix | Method | Override it when | Return value | @@ -106,8 +129,6 @@ ACP Kit models those callable shapes locally and passes them through the public That means: -- bridge extension code should import history-processor aliases from - `pydantic_acp`, not from `pydantic_ai._history_processor` - the adapter is no longer directly coupled to upstream private history-processor imports @@ -239,6 +260,19 @@ Use it for: It is the bridge most real coding-agent setups start with. +### `PrepareOutputToolsBridge` + +Shapes output-tool availability per mode. + +Use it when: + +- structured-output tools should be filtered separately from normal function tools +- ACP session metadata should expose the active output-tool mode +- output-tool preparation should emit ACP-visible progress and failure updates + +This mirrors `PrepareToolsBridge`, but targets Pydantic AI's +`PrepareOutputTools` capability. + ### `ThinkingBridge` Exposes Pydantic AI’s `Thinking` capability through ACP session config. @@ -254,6 +288,10 @@ Adds a `Hooks` capability into the active agent. Useful when you want ACP-visible hook updates that come from bridge-owned hooks rather than only from hooks already attached to the source agent. +The bridge covers the current Pydantic AI hook surface, including tool +preparation, output-tool preparation, output validation, output processing, and +deferred tool-call observation. + You can also suppress noisy default hook rendering with: ```python diff --git a/docs/examples/deepagents.md b/docs/examples/deepagents.md index a35726f..eb3e53a 100644 --- a/docs/examples/deepagents.md +++ b/docs/examples/deepagents.md @@ -8,6 +8,7 @@ This example is the maintained DeepAgents-facing showcase for `langchain-acp`. It demonstrates: +- a Codex-backed `ChatOpenAI` model created through `codex-auth-helper` - wiring a DeepAgents graph through `langchain-acp` - `DeepAgentsCompatibilityBridge` - `DeepAgentsProjectionMap` @@ -30,6 +31,12 @@ Run it: uv run python -m examples.langchain.deepagents_graph ``` +Required local state: + +```text +~/.codex/auth.json +``` + If you want the module-level compiled graph directly, the example exports `graph` when `deepagents` is installed: ```bash diff --git a/docs/examples/langchain-codex.md b/docs/examples/langchain-codex.md index a31a0d1..a0425f1 100644 --- a/docs/examples/langchain-codex.md +++ b/docs/examples/langchain-codex.md @@ -10,6 +10,15 @@ This example demonstrates the helper-to-adapter path for LangChain: - `langchain.agents.create_agent(...)` owns the graph - `langchain-acp` exposes that graph through ACP +The model factory call must pass `instructions=` explicitly: + +```python +model = create_codex_chat_openai( + "gpt-5.4", + instructions="You are a helpful coding assistant.", +) +``` + Run it: ```bash @@ -22,6 +31,12 @@ Required local state: ~/.codex/auth.json ``` +Override the default model when needed: + +```bash +CODEX_MODEL=gpt-5.4-mini uv run python -m examples.langchain.codex_graph +``` + If you have not logged in yet: ```bash diff --git a/docs/examples/langchain-workspace.md b/docs/examples/langchain-workspace.md index be6556a..0202b05 100644 --- a/docs/examples/langchain-workspace.md +++ b/docs/examples/langchain-workspace.md @@ -8,6 +8,7 @@ This example is the maintained plain-LangChain showcase. It demonstrates: +- a Codex-backed `ChatOpenAI` model created through `codex-auth-helper` - a module-level `graph`, `config`, and `main()` - a session-aware `graph_from_session(...)` factory - `acpkit run examples.langchain.workspace_graph:graph` @@ -21,6 +22,18 @@ Run it: uv run python -m examples.langchain.workspace_graph ``` +Required local state: + +```text +~/.codex/auth.json +``` + +Override the default model when needed: + +```bash +CODEX_MODEL=gpt-5.4-mini uv run python -m examples.langchain.workspace_graph +``` + Or expose the graph directly through the root CLI: ```bash diff --git a/docs/getting-started/langchain-quickstart.md b/docs/getting-started/langchain-quickstart.md index 091ef08..e4b7e0d 100644 --- a/docs/getting-started/langchain-quickstart.md +++ b/docs/getting-started/langchain-quickstart.md @@ -29,7 +29,10 @@ from codex_auth_helper import create_codex_chat_openai from langchain.agents import create_agent graph = create_agent( - model=create_codex_chat_openai("gpt-5.4"), + model=create_codex_chat_openai( + "gpt-5.4", + instructions="Answer directly and keep responses short.", + ), tools=[], name="codex-graph", ) @@ -38,6 +41,9 @@ graph = create_agent( That path keeps the model on the OpenAI Responses API while reusing `~/.codex/auth.json` and the helper's refresh flow. +`create_codex_chat_openai(...)` requires `instructions=`. Pass them at model +construction time, including inside `graph_factory=` flows. + ## 2. Expose The Graph Through ACP Wrap the graph with `run_acp(...)`: diff --git a/docs/getting-started/pydantic-quickstart.md b/docs/getting-started/pydantic-quickstart.md index 0f8844f..4246cf2 100644 --- a/docs/getting-started/pydantic-quickstart.md +++ b/docs/getting-started/pydantic-quickstart.md @@ -25,6 +25,25 @@ def describe_project() -> str: Nothing here is ACP-specific yet. +If the agent should reuse an existing local Codex login, build the model through +`codex-auth-helper` and set instructions explicitly at the factory layer: + +```python +from codex_auth_helper import create_codex_responses_model +from pydantic_ai import Agent + +model = create_codex_responses_model( + "gpt-5.4", + instructions="Answer directly and keep responses short.", +) + +agent = Agent(model, name="codex-agent") +``` + +On the Pydantic path, `Agent(instructions=...)` is also valid when you want +agent-specific instructions in addition to the factory default. Do not rely on +an implicit Codex instruction value. + ## 2. Expose It Through ACP Wrap the agent with `run_acp(...)`: diff --git a/docs/helpers.md b/docs/helpers.md index 79147b9..1cea765 100644 --- a/docs/helpers.md +++ b/docs/helpers.md @@ -68,10 +68,17 @@ Pydantic AI: from codex_auth_helper import create_codex_responses_model from pydantic_ai import Agent -model = create_codex_responses_model("gpt-5.4") -agent = Agent(model, instructions="You are a helpful coding assistant.") +model = create_codex_responses_model( + "gpt-5.4", + instructions="You are a helpful coding assistant.", +) +agent = Agent(model) ``` +Pass `instructions=` explicitly to `create_codex_responses_model(...)`. On the +Pydantic path you can also add `Agent(instructions=...)` when you want +agent-owned instructions on top of the factory default. + LangChain: ```python @@ -79,12 +86,18 @@ from codex_auth_helper import create_codex_chat_openai from langchain.agents import create_agent graph = create_agent( - model=create_codex_chat_openai("gpt-5.4"), + model=create_codex_chat_openai( + "gpt-5.4", + instructions="You are a helpful coding assistant.", + ), tools=[], name="codex-graph", ) ``` +`create_codex_chat_openai(...)` requires `instructions=`. There is no implicit +default on the LangChain path. + ACP-side usage looks the same: ```python @@ -93,7 +106,10 @@ from pydantic_ai import Agent from pydantic_acp import run_acp agent = Agent( - create_codex_responses_model("gpt-5.4"), + create_codex_responses_model( + "gpt-5.4", + instructions="You are a helpful ACP coding assistant.", + ), name="codex-agent", ) diff --git a/docs/index.md b/docs/index.md index 41bfb53..5a552a3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,7 +8,7 @@ title: ACP Kit ACP Kit is the adapter toolkit and monorepo for exposing existing agent runtimes through ACP without inventing runtime behavior the source framework does not actually own. -Today the repo ships two production-grade adapter families: +Today the repo ships two maintained adapter families: - [`pydantic-acp`](pydantic-acp.md) for `pydantic_ai.Agent` - [`langchain-acp`](langchain-acp.md) for LangChain, LangGraph, and DeepAgents graphs @@ -34,7 +34,7 @@ Three ideas drive the SDK: | Package | Purpose | Start here | |---|---|---| -| [`pydantic-acp`](pydantic-acp.md) | production-grade ACP adapter for `pydantic_ai.Agent` | If your runtime starts from a `pydantic_ai.Agent` | +| [`pydantic-acp`](pydantic-acp.md) | maintained ACP adapter for `pydantic_ai.Agent` | If your runtime starts from a `pydantic_ai.Agent` | | [`langchain-acp`](langchain-acp.md) | graph-centric ACP adapter for LangChain, LangGraph, and DeepAgents | If your runtime already produces a compiled graph | | [`acpkit`](cli.md) | CLI target resolution, launch helpers, adapter dispatch | If you want `acpkit run ...` or `acpkit launch ...` | | [`helpers`](helpers.md) | supporting packages such as `codex-auth-helper` and `acpremote` | If you need transport or model-construction helpers around an adapter | diff --git a/docs/langchain-acp.md b/docs/langchain-acp.md index a632a4c..eb4c393 100644 --- a/docs/langchain-acp.md +++ b/docs/langchain-acp.md @@ -35,7 +35,10 @@ from langchain.agents import create_agent from langchain_acp import run_acp graph = create_agent( - model=create_codex_chat_openai("gpt-5.4"), + model=create_codex_chat_openai( + "gpt-5.4", + instructions="You are a helpful coding assistant.", + ), tools=[], name="codex-graph", ) @@ -69,6 +72,8 @@ Use `graph_factory=` when ACP session state should rebuild the upstream graph. T If model construction depends on a local Codex login, pair this adapter with `codex-auth-helper`. The helper owns auth parsing, refresh, and Responses-backed `ChatOpenAI` construction; `langchain-acp` only owns ACP adaptation. +`create_codex_chat_openai(...)` requires `instructions=`, including when it is +called inside `graph_factory=...`. ## What The Adapter Owns @@ -77,10 +82,13 @@ If model construction depends on a local Codex login, pair this adapter with - `AdapterConfig` - explicit session stores and transcript replay - provider-owned models, modes, and config options +- prompt capability advertisement - native ACP plan state with `TaskPlan` -- approval bridging +- approval bridging with projection-aware permission cards - capability bridges +- built-in and host-defined slash commands - projection maps and event projection maps +- external hook event projection - ACP-facing type exports in `langchain_acp.types` The important difference is upstream shape, not ACP Kit architecture. On the LangChain side the adapter deals in graphs and middleware instead of model profiles and tool preparers. @@ -128,6 +136,51 @@ The point is not to make the adapter magical. The point is to keep the host, the graph, and the ACP surface aligned without inventing runtime state the graph cannot really honor. +## Prompt Capabilities And Slash Commands + +Prompt capability advertisement is configurable instead of hardcoded: + +```python +from langchain_acp import AdapterConfig, AdapterPromptCapabilities + +config = AdapterConfig( + prompt_capabilities=AdapterPromptCapabilities( + audio=False, + image=False, + embedded_context=True, + ) +) +``` + +The adapter also owns an ACP-native slash-command layer: + +- mode commands such as `/ask` +- `/model` +- `/tools` +- `/mcp-servers` +- custom host commands through `slash_command_provider` + +```python +from acp.schema import AvailableCommand +from langchain_acp import ( + AdapterConfig, + SlashCommandResult, + StaticSlashCommand, + StaticSlashCommandProvider, +) + +config = AdapterConfig( + slash_command_provider=StaticSlashCommandProvider( + commands=[ + StaticSlashCommand( + command=AvailableCommand(name="ping", description="Return pong."), + handler=lambda _request: SlashCommandResult(text="pong"), + ) + ] + ) +) +``` + ## Session Lifecycle And Replay Session lifecycle is first-class: @@ -204,10 +257,23 @@ The adapter surface is: - `ApprovalBridge` - `NativeApprovalBridge` +- `ProjectionAwareApprovalBridge` +- `PermissionToolCallBuilder` +- `ApprovalPolicyStore` - ACP permission requests and resume flow When the runtime really pauses for approval, the ACP session pauses for approval too. +Remembered approval choices and permission card rendering live on `NativeApprovalBridge`: + +```python +from langchain_acp import NativeApprovalBridge + +config = AdapterConfig( + approval_bridge=NativeApprovalBridge(enable_persistent_choices=True), +) +``` + ## Capability Bridges And Graph Build Contributions ACP Kit's bridge architecture remains intact in the LangChain adapter. @@ -219,6 +285,7 @@ Built-in bridges: - `ConfigOptionsBridge` - `ToolSurfaceBridge` - `DeepAgentsCompatibilityBridge` +- `ExternalHookEventBridge` Graph-build contributions are aggregated through: diff --git a/docs/langchain-acp/session-state.md b/docs/langchain-acp/session-state.md index 48a05b3..1593aa1 100644 --- a/docs/langchain-acp/session-state.md +++ b/docs/langchain-acp/session-state.md @@ -34,6 +34,10 @@ Stored session state includes: - session-local mode id - config values - plan state + +`FileSessionStore` persists those values as local JSON files. File-backed session ids are restricted +to ASCII letters, digits, `_`, and `-`, with a 128-character limit, so a client-supplied session id +cannot escape the configured store root. - MCP server definitions - transcript updates - metadata diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 41deec1..f802f65 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -465,6 +465,25 @@ def describe_project() -> str: Nothing here is ACP-specific yet. +If the agent should reuse an existing local Codex login, build the model through +`codex-auth-helper` and set instructions explicitly at the factory layer: + +```python +from codex_auth_helper import create_codex_responses_model +from pydantic_ai import Agent + +model = create_codex_responses_model( + "gpt-5.4", + instructions="Answer directly and keep responses short.", +) + +agent = Agent(model, name="codex-agent") +``` + +On the Pydantic path, `Agent(instructions=...)` is also valid when you want +agent-specific instructions in addition to the factory default. Do not rely on +an implicit Codex instruction value. + ## 2. Expose It Through ACP Wrap the agent with `run_acp(...)`: @@ -672,7 +691,10 @@ from codex_auth_helper import create_codex_chat_openai from langchain.agents import create_agent graph = create_agent( - model=create_codex_chat_openai("gpt-5.4"), + model=create_codex_chat_openai( + "gpt-5.4", + instructions="Answer directly and keep responses short.", + ), tools=[], name="codex-graph", ) @@ -681,6 +703,9 @@ graph = create_agent( That path keeps the model on the OpenAI Responses API while reusing `~/.codex/auth.json` and the helper's refresh flow. +`create_codex_chat_openai(...)` requires `instructions=`. Pass them at model +construction time, including inside `graph_factory=` flows. + ## 2. Expose The Graph Through ACP Wrap the graph with `run_acp(...)`: @@ -1104,6 +1129,25 @@ run_acp(agent=agent) This is the fastest path from a normal `pydantic_ai.Agent` to a working ACP server. +If the agent should reuse an existing local Codex login, build the model through +`codex-auth-helper` and pass explicit instructions at factory construction time: + +```python +from codex_auth_helper import create_codex_responses_model +from pydantic_ai import Agent + +model = create_codex_responses_model( + "gpt-5.4", + instructions="You are a helpful coding assistant.", +) + +agent = Agent(model, name="codex-agent") +``` + +On the Pydantic path, `Agent(instructions=...)` can still be layered on top for +agent-owned instructions, but the Codex factory should always receive explicit +`instructions=...`. + ### `create_acp_agent(...)` Use `create_acp_agent(...)` when another runtime should own transport lifecycle but you still want the adapter assembly: @@ -1182,7 +1226,9 @@ By default, the adapter can own: - native ACP plan state - thinking effort config - approval flow through an approval bridge +- projection-aware permission prompt rendering and remembered approval policies - generic or rich projected tool rendering +- host-defined slash commands and prompt capability advertisement The built-in ownership path is usually enough for: @@ -1316,24 +1362,25 @@ If you are integrating `pydantic-acp` in a real product: ## Version Compatibility And Private Upstream APIs -`pydantic-acp` currently pins `pydantic-ai-slim==1.83.0`. +`pydantic-acp` currently pins `pydantic-ai-slim==1.92.0`. That is not accidental. The adapter relies on a specific, tested Pydantic AI surface and should still be upgraded deliberately. -However, ACP Kit no longer imports Pydantic AI private history-processor -modules directly. History processor support is expressed through ACP Kit's own -callable aliases and passed into the public -`Agent(..., history_processors=...)` interface. +The current compatibility surface includes function-tool preparation, +output-tool preparation, output validation/processing hooks, +deferred-tool-call hooks, run metadata, and conversation IDs. + +ACP Kit also no longer imports Pydantic AI private history-processor modules +directly. History processor support is expressed through ACP Kit's own callable +aliases and passed into the public `Agent(..., history_processors=...)` +interface. What this means in practice: - the adapter is less exposed to private upstream type-module churn -- upgrades are still compatibility work, but the history-processor integration - is no longer a direct private-import dependency -- extension code should use `HistoryProcessorCallable`, - `HistoryProcessorPlain`, or `HistoryProcessorContextual` from `pydantic_acp` - rather than importing from `pydantic_ai._history_processor` +- upgrades are still compatibility work, but Pydantic AI integration points stay + isolated behind ACP Kit bridge and runtime seams ### LangChain ACP Overview URL: https://vcoderun.github.io/acpkit/langchain-acp/ @@ -1376,7 +1423,10 @@ from langchain.agents import create_agent from langchain_acp import run_acp graph = create_agent( - model=create_codex_chat_openai("gpt-5.4"), + model=create_codex_chat_openai( + "gpt-5.4", + instructions="You are a helpful coding assistant.", + ), tools=[], name="codex-graph", ) @@ -1408,6 +1458,8 @@ Use `graph_factory=` when ACP session state should rebuild the upstream graph. T If model construction depends on a local Codex login, pair this adapter with `codex-auth-helper`. The helper owns auth parsing, refresh, and Responses-backed `ChatOpenAI` construction; `langchain-acp` only owns ACP adaptation. +`create_codex_chat_openai(...)` requires `instructions=`, including when it is +called inside `graph_factory=...`. ## What The Adapter Owns @@ -1416,10 +1468,13 @@ If model construction depends on a local Codex login, pair this adapter with - `AdapterConfig` - explicit session stores and transcript replay - provider-owned models, modes, and config options +- prompt capability advertisement - native ACP plan state with `TaskPlan` -- approval bridging +- approval bridging with projection-aware permission cards - capability bridges +- built-in and host-defined slash commands - projection maps and event projection maps +- external hook event projection - ACP-facing type exports in `langchain_acp.types` The important difference is upstream shape, not ACP Kit architecture. On the LangChain side the adapter deals in graphs and middleware instead of model profiles and tool preparers. @@ -1465,6 +1520,51 @@ The point is not to make the adapter magical. The point is to keep the host, the graph, and the ACP surface aligned without inventing runtime state the graph cannot really honor. +## Prompt Capabilities And Slash Commands + +Prompt capability advertisement is configurable instead of hardcoded: + +```python +from langchain_acp import AdapterConfig, AdapterPromptCapabilities + +config = AdapterConfig( + prompt_capabilities=AdapterPromptCapabilities( + audio=False, + image=False, + embedded_context=True, + ) +) +``` + +The adapter also owns an ACP-native slash-command layer: + +- mode commands such as `/ask` +- `/model` +- `/tools` +- `/mcp-servers` +- custom host commands through `slash_command_provider` + +```python +from acp.schema import AvailableCommand +from langchain_acp import ( + AdapterConfig, + SlashCommandResult, + StaticSlashCommand, + StaticSlashCommandProvider, +) + +config = AdapterConfig( + slash_command_provider=StaticSlashCommandProvider( + commands=[ + StaticSlashCommand( + command=AvailableCommand(name="ping", description="Return pong."), + handler=lambda _request: SlashCommandResult(text="pong"), + ) + ] + ) +) +``` + ## Session Lifecycle And Replay Session lifecycle is first-class: @@ -1541,10 +1641,23 @@ The adapter surface is: - `ApprovalBridge` - `NativeApprovalBridge` +- `ProjectionAwareApprovalBridge` +- `PermissionToolCallBuilder` +- `ApprovalPolicyStore` - ACP permission requests and resume flow When the runtime really pauses for approval, the ACP session pauses for approval too. +Remembered approval choices and permission card rendering live on `NativeApprovalBridge`: + +```python +from langchain_acp import NativeApprovalBridge + +config = AdapterConfig( + approval_bridge=NativeApprovalBridge(enable_persistent_choices=True), +) +``` + ## Capability Bridges And Graph Build Contributions ACP Kit's bridge architecture remains intact in the LangChain adapter. @@ -1556,6 +1669,7 @@ Built-in bridges: - `ConfigOptionsBridge` - `ToolSurfaceBridge` - `DeepAgentsCompatibilityBridge` +- `ExternalHookEventBridge` Graph-build contributions are aggregated through: @@ -1734,6 +1848,8 @@ Use it to decide: | `approval_bridge` | `ApprovalBridge \| None` | Live ACP approval workflow | | `approval_state_provider` | `ApprovalStateProvider \| None` | Extra approval metadata exposed into session metadata | | `capability_bridges` | `Sequence[CapabilityBridge]` | ACP-visible runtime extensions | +| `prompt_capabilities` | `AdapterPromptCapabilities` | ACP prompt capability advertisement for audio, image, and embedded context input | +| `slash_command_provider` | `SlashCommandProvider \| None` | Extra host-defined slash commands exposed and handled by the adapter | | `session_store` | `SessionStore` | Backing store for ACP sessions | | `host_access_policy` | `HostAccessPolicy \| None` | Shared host file and terminal access policy for integrations that want one typed guardrail surface | | `projection_maps` | `Sequence[ProjectionMap]` | Richer tool rendering | @@ -1744,6 +1860,30 @@ Use it to decide: | `enable_model_config_option` | `bool` | Controls whether the model picker is mirrored as an ACP config option | | `replay_history_on_load` | `bool` | Replays transcript/message history when a session is loaded | +## Prompt Capability Advertisement + +Use `AdapterPromptCapabilities` when the ACP client should see a narrower prompt input surface than the adapter can parse by default: + +```python +from pydantic_acp import AdapterConfig, AdapterPromptCapabilities + +config = AdapterConfig( + prompt_capabilities=AdapterPromptCapabilities( + audio=False, + image=False, + embedded_context=False, + ), +) +``` + +This changes ACP initialization metadata only. It does not rewrite prompt parsing rules. + +## Custom Slash Commands + +Use `slash_command_provider` when the host wants to advertise and handle application-specific slash commands without replacing the built-in `/model`, `/tools`, `/hooks`, `/mcp-servers`, or mode command behavior. + +Command names must be lowercase slash-compatible ids such as `diagnose` or `refresh-index`; they cannot collide with built-ins or active mode ids. + ## A Practical Configuration ```python @@ -2101,7 +2241,7 @@ These controls exist to keep session state explicit and inspectable from the cli ## Slash Commands -The adapter exposes a small fixed command set plus dynamic mode commands. +The adapter exposes a small fixed command set, dynamic mode commands, and optional host-defined commands. ### Fixed commands @@ -2136,6 +2276,28 @@ Mode ids must remain compatible with slash-command addressing: - they cannot collide with reserved commands such as `model`, `thinking`, `tools`, `hooks`, or `mcp-servers` - they should stay specific enough that the command still reads clearly in the UI +### Custom Commands + +Configure `AdapterConfig(slash_command_provider=...)` to add host-owned commands: + +```python +from acp.schema import AvailableCommand +from pydantic_acp import SlashCommandResult, StaticSlashCommand, StaticSlashCommandProvider + +provider = StaticSlashCommandProvider( + commands=[ + StaticSlashCommand( + command=AvailableCommand(name="diagnose", description="Run host diagnostics."), + handler=lambda request: SlashCommandResult(text="Diagnostics queued."), + ) + ] +) +``` + +Custom handlers receive `SlashCommandRequest` with the parsed command name, optional raw argument string, session, and active agent. Returning `None` or `SlashCommandResult(handled=False)` falls through to normal model execution. + +`SlashCommandResult.refresh_session_surface` defaults to `True` so commands that mutate visible state refresh commands, config, mode, plan, and session metadata after they run. + ## Mode Changes Update ACP State Mode commands do more than print text. @@ -2319,6 +2481,39 @@ These are available when the current mode also supports plan progress: `acp_get_plan` returns numbered entries, and those numbers are intentionally **1-based**. +## Projection-aware Approval Prompts + +`NativeApprovalBridge` can render permission requests through a `PermissionToolCallBuilder`. + +The default builder uses configured projection maps for permission cards, so a projected file-write tool can show the same structured diff before approval that the transcript later shows after execution. + +Customize permission rendering by passing a builder directly to `NativeApprovalBridge`: + +```python +from pydantic_acp import NativeApprovalBridge + +approval_bridge = NativeApprovalBridge(tool_call_builder=my_builder) +``` + +The builder is intentionally owned by `NativeApprovalBridge`; it is not an `AdapterConfig` field. Custom approval bridges can keep their own permission presentation strategy. + +## Persistent Approval Policies + +`NativeApprovalBridge(enable_persistent_choices=True)` adds always-allow and always-deny options. By default, remembered policies are stored in session metadata under `approval_policies`. + +Use `ApprovalPolicyStore` to keep remembered approval policy in host storage instead: + +```python +from pydantic_acp import NativeApprovalBridge + +approval_bridge = NativeApprovalBridge( + enable_persistent_choices=True, + policy_store=my_policy_store, +) +``` + +Use `PermissionOptionSet` to change display names without changing option ids or ACP permission kinds. + ## How Native Plan State Is Enabled Mark one `PrepareToolsMode` as `plan_mode=True`: @@ -2779,6 +2974,8 @@ This is the right tool when state already belongs to the application or product | `NativePlanPersistenceProvider` | persistence callback for adapter-owned native plan state | | `ApprovalStateProvider` | extra approval metadata surfaced in session metadata | +`ApprovalStateProvider` is metadata-only. Live remembered approval decisions are owned by `ApprovalPolicyStore` on `NativeApprovalBridge`. + ## When Providers Are The Right Choice Use a provider when: @@ -2790,6 +2987,21 @@ Use a provider when: Do **not** reach for providers by default. If the adapter can own the state cleanly, built-in `AdapterConfig` fields are usually simpler. +## Approval Policy Storage + +Use `ApprovalPolicyStore` when persistent allow/reject decisions must live in host storage: + +```python +from pydantic_acp import NativeApprovalBridge + +approval_bridge = NativeApprovalBridge( + enable_persistent_choices=True, + policy_store=my_policy_store, +) +``` + +This is separate from `ApprovalStateProvider`, which only contributes session metadata for display or diagnostics. + ## Example: Host-owned Models, Modes, Config, Plan, And Approval Metadata ```python @@ -3065,6 +3277,29 @@ Use plain `CapabilityBridge` when you only need to: Use `BufferedCapabilityBridge` when the bridge also needs to emit ACP transcript updates over time. +## External Hook Events + +Use `ExternalHookEventBridge` when an integration already knows about lifecycle events and wants to project them into ACP without installing Pydantic AI hooks or writing directly to the ACP client. + +```python +from pydantic_acp import ExternalHookEventBridge, HookEvent + +bridge = ExternalHookEventBridge() +bridge.record_event( + session, + HookEvent( + event_id="before_run", + hook_name="before_run", + tool_name=None, + tool_filters=(), + raw_output="completed", + status="completed", + ), +) +``` + +The bridge buffers updates and the adapter drains them through the normal bridge manager. Its session metadata appears under `external_hooks` by default and includes emission mode, pending event count, hidden event ids, and projection title prefix. + ### Override Matrix | Method | Override it when | Return value | @@ -3134,8 +3369,6 @@ ACP Kit models those callable shapes locally and passes them through the public That means: -- bridge extension code should import history-processor aliases from - `pydantic_acp`, not from `pydantic_ai._history_processor` - the adapter is no longer directly coupled to upstream private history-processor imports @@ -3265,6 +3498,19 @@ Use it for: It is the bridge most real coding-agent setups start with. +### `PrepareOutputToolsBridge` + +Shapes output-tool availability per mode. + +Use it when: + +- structured-output tools should be filtered separately from normal function tools +- ACP session metadata should expose the active output-tool mode +- output-tool preparation should emit ACP-visible progress and failure updates + +This mirrors `PrepareToolsBridge`, but targets Pydantic AI's +`PrepareOutputTools` capability. + ### `ThinkingBridge` Exposes Pydantic AI’s `Thinking` capability through ACP session config. @@ -3280,6 +3526,10 @@ Adds a `Hooks` capability into the active agent. Useful when you want ACP-visible hook updates that come from bridge-owned hooks rather than only from hooks already attached to the source agent. +The bridge covers the current Pydantic AI hook surface, including tool +preparation, output-tool preparation, output validation, output processing, and +deferred tool-call observation. + You can also suppress noisy default hook rendering with: ```python @@ -5028,10 +5278,17 @@ Pydantic AI: from codex_auth_helper import create_codex_responses_model from pydantic_ai import Agent -model = create_codex_responses_model("gpt-5.4") -agent = Agent(model, instructions="You are a helpful coding assistant.") +model = create_codex_responses_model( + "gpt-5.4", + instructions="You are a helpful coding assistant.", +) +agent = Agent(model) ``` +Pass `instructions=` explicitly to `create_codex_responses_model(...)`. On the +Pydantic path you can also add `Agent(instructions=...)` when you want +agent-owned instructions on top of the factory default. + LangChain: ```python @@ -5039,12 +5296,18 @@ from codex_auth_helper import create_codex_chat_openai from langchain.agents import create_agent graph = create_agent( - model=create_codex_chat_openai("gpt-5.4"), + model=create_codex_chat_openai( + "gpt-5.4", + instructions="You are a helpful coding assistant.", + ), tools=[], name="codex-graph", ) ``` +`create_codex_chat_openai(...)` requires `instructions=`. There is no implicit +default on the LangChain path. + ACP-side usage looks the same: ```python @@ -5053,7 +5316,10 @@ from pydantic_ai import Agent from pydantic_acp import run_acp agent = Agent( - create_codex_responses_model("gpt-5.4"), + create_codex_responses_model( + "gpt-5.4", + instructions="You are a helpful ACP coding assistant.", + ), name="codex-agent", ) @@ -5439,6 +5705,7 @@ This example is the maintained plain-LangChain showcase. It demonstrates: +- a Codex-backed `ChatOpenAI` model created through `codex-auth-helper` - a module-level `graph`, `config`, and `main()` - a session-aware `graph_from_session(...)` factory - `acpkit run examples.langchain.workspace_graph:graph` @@ -5452,6 +5719,18 @@ Run it: uv run python -m examples.langchain.workspace_graph ``` +Required local state: + +```text +~/.codex/auth.json +``` + +Override the default model when needed: + +```bash +CODEX_MODEL=gpt-5.4-mini uv run python -m examples.langchain.workspace_graph +``` + Or expose the graph directly through the root CLI: ```bash @@ -5479,6 +5758,7 @@ This example is the maintained DeepAgents-facing showcase for `langchain-acp`. It demonstrates: +- a Codex-backed `ChatOpenAI` model created through `codex-auth-helper` - wiring a DeepAgents graph through `langchain-acp` - `DeepAgentsCompatibilityBridge` - `DeepAgentsProjectionMap` @@ -5501,6 +5781,12 @@ Run it: uv run python -m examples.langchain.deepagents_graph ``` +Required local state: + +```text +~/.codex/auth.json +``` + If you want the module-level compiled graph directly, the example exports `graph` when `deepagents` is installed: ```bash @@ -5904,6 +6190,8 @@ This page documents the public surface re-exported by `pydantic_acp`. ::: pydantic_acp.AdapterModel +::: pydantic_acp.AdapterPromptCapabilities + ::: pydantic_acp.AcpSessionContext ::: pydantic_acp.JsonValue @@ -5946,6 +6234,14 @@ This page documents the public surface re-exported by `pydantic_acp`. ::: pydantic_acp.ApprovalStateProvider +::: pydantic_acp.ApprovalPolicy + +::: pydantic_acp.ApprovalPolicyStore + +::: pydantic_acp.SessionMetadataApprovalPolicyStore + +::: pydantic_acp.PermissionOptionSet + ## Bridge Classes ::: pydantic_acp.CapabilityBridge @@ -5956,10 +6252,18 @@ This page documents the public surface re-exported by `pydantic_acp`. ::: pydantic_acp.PrepareToolsMode +::: pydantic_acp.PrepareOutputToolsBridge + +::: pydantic_acp.PrepareOutputToolsMode + ::: pydantic_acp.ThinkingBridge ::: pydantic_acp.HookBridge +::: pydantic_acp.ExternalHookEventBridge + +::: pydantic_acp.EventEmissionMode + ::: pydantic_acp.HistoryProcessorBridge ::: pydantic_acp.ThreadExecutorBridge @@ -6006,6 +6310,36 @@ This page documents the public surface re-exported by `pydantic_acp`. ::: pydantic_acp.CompositeProjectionMap +::: pydantic_acp.ProjectionAwareToolClassifier + +## Approval Presentation + +::: pydantic_acp.PermissionRequestContext + +::: pydantic_acp.PermissionToolCallBuilder + +::: pydantic_acp.DefaultPermissionToolCallBuilder + +::: pydantic_acp.NativeApprovalBridge + +::: pydantic_acp.ProjectionAwareApprovalBridge + +::: pydantic_acp.supports_projection_aware_approval_bridge + +## Slash Commands + +::: pydantic_acp.SlashCommandRequest + +::: pydantic_acp.SlashCommandResult + +::: pydantic_acp.SlashCommandProvider + +::: pydantic_acp.StaticSlashCommand + +::: pydantic_acp.StaticSlashCommandProvider + +::: pydantic_acp.SlashCommandHandler + ## Projection Helpers ::: pydantic_acp.truncate_text diff --git a/docs/llms.txt b/docs/llms.txt index c3b511b..482ddd5 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -41,28 +41,28 @@ Today the repo ships production-grade adapters for both Pydantic AI and the Lang Summary: Graph-centric adapter overview, production config examples, DeepAgents compatibility, migration notes, and maintained LangChain examples. - [AdapterConfig](https://vcoderun.github.io/acpkit/pydantic-acp/adapter-config/) Source: `docs/pydantic-acp/adapter-config.md` - Summary: Field-by-field guide to runtime configuration, ownership, and adapter behavior. + Summary: Field-by-field guide to runtime configuration, prompt capabilities, ownership, and adapter behavior. - [Session State and Lifecycle](https://vcoderun.github.io/acpkit/pydantic-acp/session-state/) Source: `docs/pydantic-acp/session-state.md` Summary: Session stores, replay semantics, persistence, and state transitions. - [Models, Modes, and Slash Commands](https://vcoderun.github.io/acpkit/pydantic-acp/runtime-controls/) Source: `docs/pydantic-acp/runtime-controls.md` - Summary: Model selection, dynamic mode switching, thinking effort, and slash command semantics. + Summary: Model selection, dynamic mode switching, custom slash commands, thinking effort, and slash command semantics. - [Plans, Thinking, and Approvals](https://vcoderun.github.io/acpkit/pydantic-acp/plans-thinking-approvals/) Source: `docs/pydantic-acp/plans-thinking-approvals.md` - Summary: Native plan state, approval flows, cancellation, and thinking capability behavior. + Summary: Native plan state, approval flows, permission presentation, approval policy storage, cancellation, and thinking capability behavior. - [Prompt Resources and Context](https://vcoderun.github.io/acpkit/pydantic-acp/prompt-resources/) Source: `docs/pydantic-acp/prompt-resources.md` Summary: Resource links, embedded context, Zed selections, branch diffs, and multimodal prompt input behavior. - [Providers](https://vcoderun.github.io/acpkit/providers/) Source: `docs/providers.md` - Summary: Host-owned models, modes, config, plan persistence, and approval metadata patterns. + Summary: Host-owned models, modes, config, plan persistence, approval metadata, and approval policy ownership patterns. - [Bridges](https://vcoderun.github.io/acpkit/bridges/) Source: `docs/bridges.md` - Summary: Capability bridges for prepare-tools, thinking, hooks, MCP metadata, and history processors. + Summary: Capability bridges for prepare-tools, thinking, hooks, external hook events, MCP metadata, and history processors. - [Host Backends and Projections](https://vcoderun.github.io/acpkit/host-backends/) Source: `docs/host-backends.md` - Summary: Client-backed filesystem, terminal execution, and projection map rendering. + Summary: Client-backed filesystem, terminal execution, projection map rendering, search/list projection, and tool classification. - [LangChain AdapterConfig](https://vcoderun.github.io/acpkit/langchain-acp/adapter-config/) Source: `docs/langchain-acp/adapter-config.md` Summary: Field-by-field guide to graph-centric adapter configuration, projection maps, and runtime ownership. @@ -128,7 +128,7 @@ Today the repo ships production-grade adapters for both Pydantic AI and the Lang Summary: API reference for the graph adapter package, providers, bridges, plans, and projection helpers. - [pydantic_acp API](https://vcoderun.github.io/acpkit/api/pydantic_acp/) Source: `docs/api/pydantic_acp.md` - Summary: API reference for the adapter package, session stores, providers, bridges, and helpers. + Summary: API reference for the adapter package, session stores, providers, bridges, approval policy/presentation seams, slash commands, projections, and helpers. - [codex_auth_helper API](https://vcoderun.github.io/acpkit/api/codex_auth_helper/) Source: `docs/api/codex_auth_helper.md` Summary: API reference for Codex auth state, client construction, and Responses model helpers. diff --git a/docs/projection-cookbook.md b/docs/projection-cookbook.md index 05af777..70f2514 100644 --- a/docs/projection-cookbook.md +++ b/docs/projection-cookbook.md @@ -118,6 +118,39 @@ Use these helpers as building blocks: - policy helpers decide whether a caution banner exists - text helpers decide how much content to show + +## Filesystem Search And List Output + +`FileSystemProjectionMap` can also project configured search or list tools: + +```python +from pydantic_acp import FileSystemProjectionMap + +projection = FileSystemProjectionMap( + default_search_tool="list_files", + search_path_arg="path", + search_pattern_arg="pattern", + render_search_results_as_tree=True, +) +``` + +When tree rendering is enabled, line-based path output is rendered as a readable tree. Failed or status-like output stays plain text. Dot directories are hidden by default while root-level dot files remain visible. + +## Projection-aware Classification + +Use `ProjectionAwareToolClassifier` when configured projection names should also control ACP tool kind: + +```python +from pydantic_acp import ProjectionAwareToolClassifier +from pydantic_acp.projection import DefaultToolClassifier + +classifier = ProjectionAwareToolClassifier( + base_classifier=DefaultToolClassifier(), + projection_maps=[projection], +) +``` + +This is opt-in. Unknown tools still fall back to the base classifier. - diff helpers shape file changes - terminal status helpers normalize exit information diff --git a/docs/providers.md b/docs/providers.md index 9945407..174f4c8 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -19,6 +19,8 @@ This is the right tool when state already belongs to the application or product | `NativePlanPersistenceProvider` | persistence callback for adapter-owned native plan state | | `ApprovalStateProvider` | extra approval metadata surfaced in session metadata | +`ApprovalStateProvider` is metadata-only. Live remembered approval decisions are owned by `ApprovalPolicyStore` on `NativeApprovalBridge`. + ## When Providers Are The Right Choice Use a provider when: @@ -30,6 +32,21 @@ Use a provider when: Do **not** reach for providers by default. If the adapter can own the state cleanly, built-in `AdapterConfig` fields are usually simpler. +## Approval Policy Storage + +Use `ApprovalPolicyStore` when persistent allow/reject decisions must live in host storage: + +```python +from pydantic_acp import NativeApprovalBridge + +approval_bridge = NativeApprovalBridge( + enable_persistent_choices=True, + policy_store=my_policy_store, +) +``` + +This is separate from `ApprovalStateProvider`, which only contributes session metadata for display or diagnostics. + ## Example: Host-owned Models, Modes, Config, Plan, And Approval Metadata ```python diff --git a/docs/pydantic-acp.md b/docs/pydantic-acp.md index ab4da53..a6ca2fc 100644 --- a/docs/pydantic-acp.md +++ b/docs/pydantic-acp.md @@ -1,6 +1,6 @@ # Pydantic ACP Overview -`pydantic-acp` is the production ACP adapter in ACP Kit. +`pydantic-acp` is the primary Pydantic AI adapter in ACP Kit. Its job is simple: keep your existing `pydantic_ai.Agent` surface intact, then expose it as an ACP server without inventing runtime state the underlying agent cannot actually honor. @@ -34,6 +34,25 @@ run_acp(agent=agent) This is the fastest path from a normal `pydantic_ai.Agent` to a working ACP server. +If the agent should reuse an existing local Codex login, build the model through +`codex-auth-helper` and pass explicit instructions at factory construction time: + +```python +from codex_auth_helper import create_codex_responses_model +from pydantic_ai import Agent + +model = create_codex_responses_model( + "gpt-5.4", + instructions="You are a helpful coding assistant.", +) + +agent = Agent(model, name="codex-agent") +``` + +On the Pydantic path, `Agent(instructions=...)` can still be layered on top for +agent-owned instructions, but the Codex factory should always receive explicit +`instructions=...`. + ### `create_acp_agent(...)` Use `create_acp_agent(...)` when another runtime should own transport lifecycle but you still want the adapter assembly: @@ -115,7 +134,9 @@ By default, the adapter can own: - native ACP plan state - thinking effort config - approval flow through an approval bridge +- projection-aware permission prompt rendering and remembered approval policies - generic or rich projected tool rendering +- host-defined slash commands and prompt capability advertisement The built-in ownership path is usually enough for: @@ -249,21 +270,22 @@ If you are integrating `pydantic-acp` in a real product: ## Version Compatibility And Private Upstream APIs -`pydantic-acp` currently pins `pydantic-ai-slim==1.83.0`. +`pydantic-acp` currently pins `pydantic-ai-slim==1.92.0`. That is not accidental. The adapter relies on a specific, tested Pydantic AI surface and should still be upgraded deliberately. -However, ACP Kit no longer imports Pydantic AI private history-processor -modules directly. History processor support is expressed through ACP Kit's own -callable aliases and passed into the public -`Agent(..., history_processors=...)` interface. +The current compatibility surface includes function-tool preparation, +output-tool preparation, output validation/processing hooks, +deferred-tool-call hooks, run metadata, and conversation IDs. + +ACP Kit also no longer imports Pydantic AI private history-processor modules +directly. History processor support is expressed through ACP Kit's own callable +aliases and passed into the public `Agent(..., history_processors=...)` +interface. What this means in practice: - the adapter is less exposed to private upstream type-module churn -- upgrades are still compatibility work, but the history-processor integration - is no longer a direct private-import dependency -- extension code should use `HistoryProcessorCallable`, - `HistoryProcessorPlain`, or `HistoryProcessorContextual` from `pydantic_acp` - rather than importing from `pydantic_ai._history_processor` +- upgrades are still compatibility work, but Pydantic AI integration points stay + isolated behind ACP Kit bridge and runtime seams diff --git a/docs/pydantic-acp/adapter-config.md b/docs/pydantic-acp/adapter-config.md index de47492..c214883 100644 --- a/docs/pydantic-acp/adapter-config.md +++ b/docs/pydantic-acp/adapter-config.md @@ -27,6 +27,8 @@ Use it to decide: | `approval_bridge` | `ApprovalBridge \| None` | Live ACP approval workflow | | `approval_state_provider` | `ApprovalStateProvider \| None` | Extra approval metadata exposed into session metadata | | `capability_bridges` | `Sequence[CapabilityBridge]` | ACP-visible runtime extensions | +| `prompt_capabilities` | `AdapterPromptCapabilities` | ACP prompt capability advertisement for audio, image, and embedded context input | +| `slash_command_provider` | `SlashCommandProvider \| None` | Extra host-defined slash commands exposed and handled by the adapter | | `session_store` | `SessionStore` | Backing store for ACP sessions | | `host_access_policy` | `HostAccessPolicy \| None` | Shared host file and terminal access policy for integrations that want one typed guardrail surface | | `projection_maps` | `Sequence[ProjectionMap]` | Richer tool rendering | @@ -37,6 +39,30 @@ Use it to decide: | `enable_model_config_option` | `bool` | Controls whether the model picker is mirrored as an ACP config option | | `replay_history_on_load` | `bool` | Replays transcript/message history when a session is loaded | +## Prompt Capability Advertisement + +Use `AdapterPromptCapabilities` when the ACP client should see a narrower prompt input surface than the adapter can parse by default: + +```python +from pydantic_acp import AdapterConfig, AdapterPromptCapabilities + +config = AdapterConfig( + prompt_capabilities=AdapterPromptCapabilities( + audio=False, + image=False, + embedded_context=False, + ), +) +``` + +This changes ACP initialization metadata only. It does not rewrite prompt parsing rules. + +## Custom Slash Commands + +Use `slash_command_provider` when the host wants to advertise and handle application-specific slash commands without replacing the built-in `/model`, `/tools`, `/hooks`, `/mcp-servers`, or mode command behavior. + +Command names must be lowercase slash-compatible ids such as `diagnose` or `refresh-index`; they cannot collide with built-ins or active mode ids. + ## A Practical Configuration ```python diff --git a/docs/pydantic-acp/plans-thinking-approvals.md b/docs/pydantic-acp/plans-thinking-approvals.md index 520890f..dbdabc8 100644 --- a/docs/pydantic-acp/plans-thinking-approvals.md +++ b/docs/pydantic-acp/plans-thinking-approvals.md @@ -31,6 +31,39 @@ These are available when the current mode also supports plan progress: `acp_get_plan` returns numbered entries, and those numbers are intentionally **1-based**. +## Projection-aware Approval Prompts + +`NativeApprovalBridge` can render permission requests through a `PermissionToolCallBuilder`. + +The default builder uses configured projection maps for permission cards, so a projected file-write tool can show the same structured diff before approval that the transcript later shows after execution. + +Customize permission rendering by passing a builder directly to `NativeApprovalBridge`: + +```python +from pydantic_acp import NativeApprovalBridge + +approval_bridge = NativeApprovalBridge(tool_call_builder=my_builder) +``` + +The builder is intentionally owned by `NativeApprovalBridge`; it is not an `AdapterConfig` field. Custom approval bridges can keep their own permission presentation strategy. + +## Persistent Approval Policies + +`NativeApprovalBridge(enable_persistent_choices=True)` adds always-allow and always-deny options. By default, remembered policies are stored in session metadata under `approval_policies`. + +Use `ApprovalPolicyStore` to keep remembered approval policy in host storage instead: + +```python +from pydantic_acp import NativeApprovalBridge + +approval_bridge = NativeApprovalBridge( + enable_persistent_choices=True, + policy_store=my_policy_store, +) +``` + +Use `PermissionOptionSet` to change display names without changing option ids or ACP permission kinds. + ## How Native Plan State Is Enabled Mark one `PrepareToolsMode` as `plan_mode=True`: diff --git a/docs/pydantic-acp/runtime-controls.md b/docs/pydantic-acp/runtime-controls.md index 9b6acc2..3e56b96 100644 --- a/docs/pydantic-acp/runtime-controls.md +++ b/docs/pydantic-acp/runtime-controls.md @@ -6,7 +6,7 @@ These controls exist to keep session state explicit and inspectable from the cli ## Slash Commands -The adapter exposes a small fixed command set plus dynamic mode commands. +The adapter exposes a small fixed command set, dynamic mode commands, and optional host-defined commands. ### Fixed commands @@ -41,6 +41,28 @@ Mode ids must remain compatible with slash-command addressing: - they cannot collide with reserved commands such as `model`, `thinking`, `tools`, `hooks`, or `mcp-servers` - they should stay specific enough that the command still reads clearly in the UI +### Custom Commands + +Configure `AdapterConfig(slash_command_provider=...)` to add host-owned commands: + +```python +from acp.schema import AvailableCommand +from pydantic_acp import SlashCommandResult, StaticSlashCommand, StaticSlashCommandProvider + +provider = StaticSlashCommandProvider( + commands=[ + StaticSlashCommand( + command=AvailableCommand(name="diagnose", description="Run host diagnostics."), + handler=lambda request: SlashCommandResult(text="Diagnostics queued."), + ) + ] +) +``` + +Custom handlers receive `SlashCommandRequest` with the parsed command name, optional raw argument string, session, and active agent. Returning `None` or `SlashCommandResult(handled=False)` falls through to normal model execution. + +`SlashCommandResult.refresh_session_surface` defaults to `True` so commands that mutate visible state refresh commands, config, mode, plan, and session metadata after they run. + ## Mode Changes Update ACP State Mode commands do more than print text. diff --git a/docs/pydantic-acp/session-state.md b/docs/pydantic-acp/session-state.md index b612231..1ed99d7 100644 --- a/docs/pydantic-acp/session-state.md +++ b/docs/pydantic-acp/session-state.md @@ -70,6 +70,7 @@ This is the recommended default for local tools and editor integrations. Current behavior: - writes use a temp file, `fsync`, and atomic replace +- session ids are restricted to ASCII letters, digits, `_`, and `-`, with a 128-character limit - the store takes a process-local lock and a filesystem advisory lock when available - malformed or partially-written session files are skipped by public load/list flows instead of crashing the whole operation - stale temp files from interrupted writes are cleaned up on startup diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 0000000..2941084 --- /dev/null +++ b/docs/security.md @@ -0,0 +1,56 @@ +# Security Guidance + +ACP Kit exposes existing agent runtimes. The adapter should stay honest about what the underlying +runtime can enforce, and deployment code should treat host files, credentials, and remote command +execution as privileged surfaces. + +## File Session Stores + +`FileSessionStore` is a local durable store, not a distributed database or a multi-host +coordination layer. + +Use it when an editor, CLI, or local service needs sessions to survive process restarts. Do not +share the same store root between unrelated users or untrusted processes. + +File-backed session ids are validated before they become filenames: + +- allowed characters are ASCII letters, digits, `_`, and `-` +- the maximum length is 128 characters +- path separators, dots, whitespace, and shell metacharacters are rejected +- malformed JSON files are skipped by load/list flows instead of crashing the adapter + +Store roots should live in a directory owned by the service user. If session content can include +sensitive prompts, tool results, or workspace paths, apply normal host-level file permissions and +backup policies. + +## Codex Auth State + +`codex-auth-helper` reads and refreshes local Codex credentials. Treat its auth state file like a +credential store. + +Current writes use a private temp file, `fsync`, atomic replace, and `0600` permissions on POSIX +systems. Operators should still keep the parent directory private and avoid copying auth state into +logs, examples, test fixtures, or container images. + +## acpremote + +`acpremote` is transport infrastructure. It can expose an existing ACP agent or a stdio command over +WebSocket, so deployment policy matters. + +Recommended defaults: + +- bind to loopback unless a reverse proxy owns TLS and authentication +- allowlist command-backed servers instead of accepting arbitrary command strings +- keep environment overrides minimal and avoid forwarding secrets that the child process does not need +- configure command termination timeouts for command-backed transports +- monitor long-running remote sessions and close idle connections at the hosting layer + +Command-backed transports terminate the child process when the WebSocket flow ends and fall back to +`kill` after the configured timeout. This prevents normal disconnect cleanup from waiting forever on +a process that ignores termination. + +## Release Workflow + +Project CI and publish workflows should install from `uv.lock` with `uv sync --frozen`. Package +publishing should use PyPI trusted publishing rather than long-lived API tokens whenever the target +PyPI project is configured for it. diff --git a/examples/langchain/.deepagents-graph/brief.md b/examples/langchain/.deepagents-graph/brief.md deleted file mode 100644 index 3421aaf..0000000 --- a/examples/langchain/.deepagents-graph/brief.md +++ /dev/null @@ -1,3 +0,0 @@ -# DeepAgents Demo - -This seeded file exists so ACP can render read and write projections. diff --git a/examples/langchain/.workspace-graph/README.md b/examples/langchain/.workspace-graph/README.md deleted file mode 100644 index 1006e3d..0000000 --- a/examples/langchain/.workspace-graph/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Workspace Graph Demo - -This seeded file lets ACP render a read diff through the LangChain example. diff --git a/examples/langchain/README.md b/examples/langchain/README.md index 0b11754..c2af105 100644 --- a/examples/langchain/README.md +++ b/examples/langchain/README.md @@ -2,14 +2,53 @@ All maintained LangChain examples live under `examples/langchain/`. +- `codex_graph.py` + Smallest Codex-backed LangChain example. It uses `codex_auth_helper.create_codex_chat_openai(...)` + directly and passes `instructions=` to the Responses request so you can see the exact LangChain + integration surface without extra wrappers. - `workspace_graph.py` - module-level `graph`, session-aware `graph_from_session(...)`, and file read/write projection for - `acpkit run ...` or remote ACP hosting + Codex-backed workspace graph with real file read/write tools, session-aware + `graph_from_session(...)`, and file projection for `langchain-acp`. The demo workspace is created + under the current working directory as `.workspace-graph/` so the graph interacts with the + workspace you launched it from instead of writing next to the example source file. - `deepagents_graph.py` - DeepAgents compatibility example with `DeepAgentsCompatibilityBridge` and `DeepAgentsProjectionMap` -- `codex_graph.py` - Codex-backed LangChain graph that uses `codex-auth-helper` to build a Responses-backed - `ChatOpenAI` model + Codex-backed DeepAgents compatibility example with real workspace tools, + `DeepAgentsCompatibilityBridge`, and `DeepAgentsProjectionMap`. Its workspace is created under the + current working directory as `.deepagents-graph/`. + +All three examples use the local Codex auth flow through `codex-auth-helper`. By default they use +`CODEX_MODEL=gpt-5.4`; set `CODEX_MODEL` before running them when you want a different Codex model. + +The LangChain-specific Codex hook is the `instructions=` argument: + +```python +from codex_auth_helper import create_codex_chat_openai + +model = create_codex_chat_openai( + "gpt-5.4", + instructions=( + "You are a careful workspace assistant. " + "Read files before editing them and explain concrete observations." + ), +) +``` + +That string is passed through to the OpenAI Responses request that backs `ChatOpenAI`. Use it when +you want repo- or task-specific system behavior without introducing another wrapper layer. +`create_codex_chat_openai(...)` requires `instructions`; there is no implicit default. + +## Model And Mode Controls + +The maintained examples also expose ACP session controls for model and mode selection. + +- `available_models` advertises the model ids the client can switch to. +- `available_modes` advertises the runtime modes the client can switch to. +- `default_model_id` and `default_mode_id` seed new sessions. +- `graph_from_session(...)` reads `session.session_model_id` and `session.session_mode_id` and + rebuilds the graph with the selected Codex model and mode-specific instructions. + +In practice that means a client can switch model or mode through ACP session controls and the next +prompt turn will run against a newly built LangChain graph that reflects those choices. ## Runnable Demo @@ -26,6 +65,10 @@ acpkit run examples.langchain.workspace_graph:graph acpkit run examples.langchain.deepagents_graph:graph ``` +If you want the session-aware graph factory path instead of the module-level `graph`, run the module +directly with `python -m ...`. That path creates the demo workspace under your launch directory and +avoids the fixed import-time graph root. + The workspace graph example also works as a remote ACP host: ```bash diff --git a/examples/langchain/codex_graph.py b/examples/langchain/codex_graph.py index 1665e4b..0d6dff4 100644 --- a/examples/langchain/codex_graph.py +++ b/examples/langchain/codex_graph.py @@ -3,19 +3,49 @@ import os from collections.abc import Callable +from acp.schema import ModelInfo, SessionMode from codex_auth_helper import create_codex_chat_openai from langchain.agents import create_agent -from langchain_acp import AdapterConfig, run_acp +from langchain_acp import AcpSessionContext, AdapterConfig, CompiledAgentGraph, run_acp __all__ = ( + "AVAILABLE_MODELS", + "AVAILABLE_MODES", "MODEL_NAME", "build_graph", "config", "describe_codex_surface", + "codex_instructions", + "graph", + "graph_from_session", "main", ) MODEL_NAME = os.getenv("CODEX_MODEL", "gpt-5.4") +AVAILABLE_MODELS = ( + ModelInfo(model_id="gpt-5.4-mini", name="GPT-5.4 Mini"), + ModelInfo(model_id=MODEL_NAME, name=f"Codex {MODEL_NAME}"), +) +AVAILABLE_MODES = ( + SessionMode(id="ask", name="Ask", description="General question answering mode."), + SessionMode(id="edit", name="Edit", description="Make focused workspace edits."), + SessionMode(id="plan", name="Plan", description="Plan first, then suggest next steps."), +) + + +def codex_instructions(*, mode_id: str) -> str: + """Return the Codex Responses instructions used by this example graph.""" + + base = ( + "You are a precise workspace assistant. " + "Use the available tools when they help, keep answers concise, " + "and explain concrete file or project observations instead of guessing." + ) + if mode_id == "edit": + return f"{base} Prefer concrete edits and summarize the changed files." + if mode_id == "plan": + return f"{base} Start with a short plan before proposing changes." + return base def describe_codex_surface() -> str: @@ -35,19 +65,57 @@ def _tools() -> tuple[Callable[[], str], ...]: return (describe_codex_surface,) -def build_graph() -> object: +def _resolved_model_name(session: AcpSessionContext | None = None) -> str: + if session is None or not session.session_model_id: + return MODEL_NAME + return session.session_model_id + + +def _resolved_mode_id(session: AcpSessionContext | None = None) -> str: + if session is None or not session.session_mode_id: + return "ask" + return session.session_mode_id + + +def build_graph( + *, + model_name: str | None = None, + mode_id: str = "ask", + graph_name: str = "codex-graph", +) -> CompiledAgentGraph: return create_agent( - model=create_codex_chat_openai(MODEL_NAME), + model=create_codex_chat_openai( + model_name or MODEL_NAME, + instructions=codex_instructions(mode_id=mode_id), + ), tools=list(_tools()), - name="codex-graph", + name=graph_name, + ) + + +def graph_from_session(session: AcpSessionContext) -> CompiledAgentGraph: + model_name = _resolved_model_name(session) + mode_id = _resolved_mode_id(session) + return build_graph( + model_name=model_name, + mode_id=mode_id, + graph_name=f"codex-{mode_id}-{session.cwd.name}", ) -config = AdapterConfig() +graph = build_graph(model_name=MODEL_NAME, mode_id="ask") + + +config = AdapterConfig( + available_models=list(AVAILABLE_MODELS), + available_modes=list(AVAILABLE_MODES), + default_model_id=AVAILABLE_MODELS[0].model_id, + default_mode_id=AVAILABLE_MODES[0].id, +) def main() -> None: - run_acp(graph=build_graph(), config=config) + run_acp(graph_factory=graph_from_session, config=config) if __name__ == "__main__": diff --git a/examples/langchain/deepagents_graph.py b/examples/langchain/deepagents_graph.py index 184744d..d9b5680 100644 --- a/examples/langchain/deepagents_graph.py +++ b/examples/langchain/deepagents_graph.py @@ -1,28 +1,36 @@ from __future__ import annotations as _annotations -from importlib import import_module +import os +from collections.abc import Callable, Sequence from importlib.util import find_spec -from itertools import cycle from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, TypeAlias, cast +from acp.schema import ModelInfo, SessionMode +from codex_auth_helper import create_codex_chat_openai from langchain_acp import ( AcpSessionContext, AdapterConfig, DeepAgentsCompatibilityBridge, DeepAgentsProjectionMap, + native_plan_tools, run_acp, ) from langchain_acp.session import utc_now -from langchain_core.language_models import GenericFakeChatModel -from langchain_core.messages import AIMessage if TYPE_CHECKING: from langchain_acp import CompiledAgentGraph + from langchain_core.tools import BaseTool + +DeepAgentTool: TypeAlias = "BaseTool | Callable[..., Any] | dict[str, Any]" __all__ = ( + "AVAILABLE_MODELS", + "AVAILABLE_MODES", + "MODEL_NAME", "WORKSPACE_ROOT", "config", + "codex_instructions", "graph", "graph_from_session", "list_workspace_files", @@ -31,7 +39,44 @@ "write_file", ) -WORKSPACE_ROOT = Path(__file__).with_name(".deepagents-graph") +WORKSPACE_ROOT = Path.cwd() / ".deepagents-graph" +MODEL_NAME = os.getenv("CODEX_MODEL", "gpt-5.4") +_SESSION_ROOT_NAME = ".deepagents-graph" +MOCK_WORKSPACE_FILES = { + "brief.md": ( + "# DeepAgents Demo\n\nThis is a mocked workspace file used by the DeepAgents example.\n" + ), + "notes.md": ( + "# Mock Notes\n\n" + "- This example uses fixed filesystem tool outputs.\n" + "- No real workspace mutation happens here.\n" + ), +} +AVAILABLE_MODELS = ( + ModelInfo(model_id="gpt-5.4-mini", name="GPT-5.4 Mini"), + ModelInfo(model_id=MODEL_NAME, name="GPT-5.4"), +) +AVAILABLE_MODES = ( + SessionMode(id="ask", name="Ask", description="Inspect the workspace and answer questions."), + SessionMode(id="edit", name="Edit", description="Make direct workspace changes."), + SessionMode(id="plan", name="Plan", description="Plan before applying changes."), +) +DEFAULT_MODEL_ID = AVAILABLE_MODELS[0].model_id +DEFAULT_MODE_ID = AVAILABLE_MODES[0].id + + +def codex_instructions(*, mode_id: str) -> str: + """Return the Codex Responses instructions used by the DeepAgents example.""" + + base = ( + "You are a workspace agent that can inspect and update files in the current demo workspace. " + "Use tools for concrete file work, keep changes scoped, and summarize what changed." + ) + if mode_id == "edit": + return f"{base} Prefer direct file edits when the user asks for changes." + if mode_id == "plan": + return f"{base} Begin with a short plan before making any edits." + return base def _deepagents_available() -> bool: @@ -56,7 +101,34 @@ def _resolve_workspace_path(path: str, *, root: Path | None = None) -> Path: if root is None: root = WORKSPACE_ROOT workspace_root = _ensure_workspace(root).resolve() - candidate = (workspace_root / path).resolve() + workspace_parent = workspace_root.parent + requested_path = Path(path) + if requested_path.is_absolute(): + absolute_candidate = requested_path.resolve() + try: + relative_to_workspace = absolute_candidate.relative_to(workspace_root) + except ValueError: + try: + relative_to_parent = absolute_candidate.relative_to(workspace_parent) + except ValueError: + candidate = absolute_candidate + else: + relative_to_parent = Path( + *( + relative_to_parent.parts[1:] + if relative_to_parent.parts + and relative_to_parent.parts[0] == workspace_root.name + else relative_to_parent.parts + ) + ) + candidate = (workspace_root / relative_to_parent).resolve() + else: + candidate = (workspace_root / relative_to_workspace).resolve() + else: + relative_path = requested_path + if requested_path.parts and requested_path.parts[0] == workspace_root.name: + relative_path = Path(*requested_path.parts[1:]) + candidate = (workspace_root / relative_path).resolve() try: candidate.relative_to(workspace_root) except ValueError as exc: @@ -64,29 +136,70 @@ def _resolve_workspace_path(path: str, *, root: Path | None = None) -> Path: return candidate +def _normalized_mock_path(path: str) -> str: + normalized = Path(path).name.strip() + if not normalized: + raise ValueError("Path must reference a mocked workspace file.") + return normalized + + def list_workspace_files() -> str: - """List files in the seeded DeepAgents example workspace.""" + """List mocked workspace files exposed by the DeepAgents example.""" - root = _ensure_workspace() - files = sorted(path.relative_to(root).as_posix() for path in root.rglob("*") if path.is_file()) - return "\n".join(files) + return "\n".join(sorted(MOCK_WORKSPACE_FILES)) def read_file(path: str) -> str: - """Read a file from the DeepAgents example workspace.""" + """Read a mocked file from the DeepAgents example workspace.""" - note_path = _resolve_workspace_path(path) - if not note_path.exists(): + normalized = _normalized_mock_path(path) + content = MOCK_WORKSPACE_FILES.get(normalized) + if content is None: raise ValueError(f"File not found: {path}") - return note_path.read_text(encoding="utf-8") + return content def write_file(path: str, content: str) -> str: - """Write a file in the DeepAgents example workspace.""" + """Pretend to write a file in the DeepAgents example workspace.""" + + normalized = _normalized_mock_path(path) + del content + return f"Mock write accepted for {normalized}" - note_path = _resolve_workspace_path(path) - note_path.write_text(content, encoding="utf-8") - return f"Wrote {path}" + +def _session_workspace_root(session: AcpSessionContext) -> Path: + return session.cwd.resolve() / _SESSION_ROOT_NAME + + +def _bind_workspace_tools( + root: Path, +) -> tuple[Callable[[], str], Callable[[str], str], Callable[[str, str], str]]: + del root + + def _list_workspace_files() -> str: + return "\n".join(sorted(MOCK_WORKSPACE_FILES)) + + _list_workspace_files.__name__ = "list_workspace_files" + _list_workspace_files.__doc__ = list_workspace_files.__doc__ + + def _read_file(path: str) -> str: + normalized = _normalized_mock_path(path) + content = MOCK_WORKSPACE_FILES.get(normalized) + if content is None: + raise ValueError(f"File not found: {path}") + return content + + _read_file.__name__ = "read_file" + _read_file.__doc__ = read_file.__doc__ + + def _write_file(path: str, content: str) -> str: + del content + normalized = _normalized_mock_path(path) + return f"Mock write accepted for {normalized}" + + _write_file.__name__ = "write_file" + _write_file.__doc__ = write_file.__doc__ + return (_list_workspace_files, _read_file, _write_file) def graph_from_session(session: AcpSessionContext) -> CompiledAgentGraph: @@ -94,15 +207,23 @@ def graph_from_session(session: AcpSessionContext) -> CompiledAgentGraph: raise RuntimeError( 'Install the optional DeepAgents dependency first: uv add "langchain-acp[deepagents]"' ) - deepagents = import_module("deepagents") - create_deep_agent = deepagents.create_deep_agent + workspace_root = _ensure_workspace(_session_workspace_root(session)).resolve() + model_name = session.session_model_id or DEFAULT_MODEL_ID or MODEL_NAME + mode_id = session.session_mode_id or DEFAULT_MODE_ID or "ask" + from deepagents import create_deep_agent + + tools: Sequence[DeepAgentTool] = [ + *_bind_workspace_tools(workspace_root), + *cast(Sequence[DeepAgentTool], native_plan_tools()), + ] return create_deep_agent( - model=GenericFakeChatModel( - messages=cycle([AIMessage(content="DeepAgents compatibility graph ready.")]) + model=create_codex_chat_openai( + model_name, + instructions=codex_instructions(mode_id=mode_id), ), - tools=[list_workspace_files, read_file, write_file], + tools=tools, interrupt_on={"write_file": True}, - name=f"deepagents-{session.cwd.name}", + name=f"deepagents-{mode_id}-{session.cwd.name}", ) @@ -120,8 +241,14 @@ def _seed_session() -> AcpSessionContext: graph = graph_from_session(_seed_session()) if _deepagents_available() else None config = AdapterConfig( + available_models=list(AVAILABLE_MODELS), + available_modes=list(AVAILABLE_MODES), capability_bridges=[DeepAgentsCompatibilityBridge()], + default_model_id=DEFAULT_MODEL_ID, + default_mode_id=DEFAULT_MODE_ID, default_plan_generation_type="tools", + enable_plan_progress_tools=True, + plan_mode_id="plan", projection_maps=[DeepAgentsProjectionMap()], ) diff --git a/examples/langchain/workspace_graph.py b/examples/langchain/workspace_graph.py index bff5a36..4337ffa 100644 --- a/examples/langchain/workspace_graph.py +++ b/examples/langchain/workspace_graph.py @@ -1,9 +1,11 @@ from __future__ import annotations as _annotations +import os from collections.abc import Callable -from itertools import cycle from pathlib import Path +from acp.schema import ModelInfo, SessionMode +from codex_auth_helper import create_codex_chat_openai from langchain.agents import create_agent from langchain_acp import ( AcpSessionContext, @@ -13,12 +15,14 @@ MemorySessionStore, run_acp, ) -from langchain_core.language_models import GenericFakeChatModel -from langchain_core.messages import AIMessage __all__ = ( + "AVAILABLE_MODELS", + "AVAILABLE_MODES", + "MODEL_NAME", "WORKSPACE_ROOT", "config", + "codex_instructions", "describe_workspace_surface", "graph", "graph_from_session", @@ -28,10 +32,35 @@ "write_workspace_note", ) -WORKSPACE_ROOT = Path(__file__).with_name(".workspace-graph") +WORKSPACE_ROOT = Path.cwd() / ".workspace-graph" _READ_TOOL = "read_workspace_note" _WRITE_TOOL = "write_workspace_note" _SESSION_ROOT_NAME = ".workspace-graph" +MODEL_NAME = os.getenv("CODEX_MODEL", "gpt-5.4") +AVAILABLE_MODELS = ( + ModelInfo(model_id="gpt-5.4-mini", name="GPT-5.4 Mini"), + ModelInfo(model_id=MODEL_NAME, name=f"Codex {MODEL_NAME}"), +) +AVAILABLE_MODES = ( + SessionMode(id="ask", name="Ask", description="Inspect the workspace and answer questions."), + SessionMode(id="edit", name="Edit", description="Make direct workspace changes."), + SessionMode(id="plan", name="Plan", description="Plan before making changes."), +) + + +def codex_instructions(*, mode_id: str) -> str: + """Return the Codex Responses instructions used by the workspace graph.""" + + base = ( + "You are a careful workspace assistant operating inside a small demo workspace. " + "Prefer reading files before changing them, keep edits focused, " + "and describe exactly which workspace files you used." + ) + if mode_id == "edit": + return f"{base} When a change is needed, write the smallest viable file update." + if mode_id == "plan": + return f"{base} Start with a short implementation plan before proposing edits." + return base def _ensure_workspace(root: Path | None = None) -> Path: @@ -71,6 +100,7 @@ def _workspace_surface_summary() -> str: return "\n".join( ( "Workspace graph features:", + f"- Codex-backed ChatOpenAI model via `codex-auth-helper` (`{MODEL_NAME}`)", "- module-level `graph` for direct `acpkit run ...:graph` exposure", "- session-aware `graph_from_session(...)` for per-session graph construction", "- file read and write projection through `FileSystemProjectionMap`", @@ -151,10 +181,19 @@ def _write_workspace_note_tool(path: str, content: str) -> str: ) -def _build_graph(root: Path, *, name: str) -> CompiledAgentGraph: +def _build_graph( + root: Path, + *, + name: str, + model_name: str, + mode_id: str, +) -> CompiledAgentGraph: _ensure_workspace(root) return create_agent( - model=GenericFakeChatModel(messages=cycle([AIMessage(content="Workspace graph ready.")])), + model=create_codex_chat_openai( + model_name, + instructions=codex_instructions(mode_id=mode_id), + ), tools=list(_bind_workspace_tools(root)), name=name, ) @@ -162,12 +201,28 @@ def _build_graph(root: Path, *, name: str) -> CompiledAgentGraph: def graph_from_session(session: AcpSessionContext) -> CompiledAgentGraph: root = _ensure_workspace(_session_workspace_root(session)).resolve() - return _build_graph(root, name=f"workspace-{session.cwd.name}") + model_name = session.session_model_id or config.default_model_id or MODEL_NAME + mode_id = session.session_mode_id or config.default_mode_id or "ask" + return _build_graph( + root, + name=f"workspace-{mode_id}-{session.cwd.name}", + model_name=model_name, + mode_id=mode_id, + ) -graph = _build_graph(_ensure_workspace().resolve(), name="workspace-graph") +graph = _build_graph( + _ensure_workspace().resolve(), + name="workspace-graph", + model_name=MODEL_NAME, + mode_id="ask", +) config = AdapterConfig( + available_models=list(AVAILABLE_MODELS), + available_modes=list(AVAILABLE_MODES), + default_model_id=AVAILABLE_MODELS[0].model_id, + default_mode_id=AVAILABLE_MODES[0].id, session_store=MemorySessionStore(), projection_maps=[ FileSystemProjectionMap( diff --git a/mkdocs.yml b/mkdocs.yml index 94fb495..3b5ba9f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -118,6 +118,7 @@ nav: - Compatibility Matrix Template: compatibility-matrix-template.md - Projection Cookbook: projection-cookbook.md - Integration Readiness: integration-readiness.md + - Security Guidance: security.md - Examples: - Overview: examples/index.md - Finance Agent: examples/finance.md diff --git a/packages/adapters/langchain-acp/README.md b/packages/adapters/langchain-acp/README.md index 6731087..1389590 100644 --- a/packages/adapters/langchain-acp/README.md +++ b/packages/adapters/langchain-acp/README.md @@ -45,6 +45,45 @@ graph = create_agent(model="openai:gpt-5", tools=[]) run_acp(graph=graph) ``` +If you are using Codex-backed LangChain models through `codex-auth-helper`, you must pass the +LangChain system behavior through the helper's `instructions=` argument. The same repo policy now +applies on the Pydantic path too: Codex-backed model factories take explicit instructions instead of +inventing an implicit default. + +```python +from codex_auth_helper import create_codex_chat_openai +from langchain.agents import create_agent + +model = create_codex_chat_openai( + "gpt-5.4", + instructions="You are a careful assistant that explains concrete workspace observations.", +) +graph = create_agent(model=model, tools=[], name="codex-graph") +``` + +That `instructions` string is required and is forwarded to the OpenAI Responses request behind +`ChatOpenAI`. See the maintained example at +. + +Use the same pattern inside `graph_factory=` paths too. If the graph is rebuilt per session, keep +the Codex system behavior explicit in the factory instead of relying on an implicit default: + +```python +from codex_auth_helper import create_codex_chat_openai +from langchain.agents import create_agent +from langchain_acp import AcpSessionContext + + +def graph_from_session(session: AcpSessionContext): + mode_name = session.session_mode_id or "ask" + model_name = session.session_model_id or "gpt-5.4-mini" + model = create_codex_chat_openai( + model_name, + instructions=f"Operate in {mode_name} mode and explain concrete workspace observations.", + ) + return create_agent(model=model, tools=[], name=f"codex-{mode_name}") +``` + If ACP session state should affect graph construction, use `graph_factory=`: ```python @@ -66,10 +105,14 @@ acp_agent = create_acp_agent(graph_factory=graph_from_session) - session stores and transcript replay - model, mode, and config-option providers +- prompt capability advertisement through `prompt_capabilities` - native plan state through `TaskPlan` - approval bridging from `HumanInTheLoopMiddleware` +- remembered approval policies and permission card rendering on `NativeApprovalBridge` - capability bridges and graph-build contributions +- built-in and host-defined slash commands - tool projection maps and event projection maps +- external hook/event projection through `ExternalHookEventBridge` - `graph`, `graph_factory`, and `graph_source` - DeepAgents compatibility helpers where they add truthful ACP behavior @@ -81,6 +124,64 @@ That means the adapter can expose: without collapsing everything into a bespoke ACP runtime. +## Runtime Controls + +The adapter now owns a small ACP-native slash-command layer instead of leaving that surface entirely to the graph: + +- mode commands such as `/ask` or `/review` when the session publishes modes +- `/model` for ACP-owned model selection +- `/tools` for the active graph tool node +- `/mcp-servers` for attached session MCP servers +- custom host commands through `slash_command_provider` + +Example: + +```python +from acp.schema import AvailableCommand +from langchain_acp import ( + AdapterConfig, + SlashCommandResult, + StaticSlashCommand, + StaticSlashCommandProvider, +) + +config = AdapterConfig( + slash_command_provider=StaticSlashCommandProvider( + commands=[ + StaticSlashCommand( + command=AvailableCommand(name="ping", description="Return pong."), + handler=lambda _request: SlashCommandResult(text="pong"), + ) + ] + ) +) +``` + +Prompt capability advertisement is also explicit now: + +```python +from langchain_acp import AdapterConfig, AdapterPromptCapabilities + +config = AdapterConfig( + prompt_capabilities=AdapterPromptCapabilities( + audio=False, + image=False, + embedded_context=True, + ) +) +``` + +If the graph uses approval middleware, remembered choices and ACP permission presentation stay on +`NativeApprovalBridge`, not on `AdapterConfig`: + +```python +from langchain_acp import NativeApprovalBridge + +config = AdapterConfig( + approval_bridge=NativeApprovalBridge(enable_persistent_choices=True), +) +``` + ## Session-owned Graph Rebuilds If ACP session state should decide which graph gets built, `graph_factory=` is the intended seam: @@ -109,6 +210,25 @@ run_acp( Use this when workspace path, mode, model, or session metadata should rebuild the graph dynamically. +The maintained examples under `examples/langchain/` also expose ACP-visible model and mode choices +through `available_models`, `available_modes`, `default_model_id`, and `default_mode_id`, then +consume `session.session_model_id` and `session.session_mode_id` inside `graph_factory=...`. + +## Session Store Notes + +Use `MemorySessionStore` for ephemeral graph sessions and `FileSessionStore` when ACP session state +should survive process restarts. The file store persists the ACP transcript, selected model, selected +mode, config values, and plan state as local JSON. + +File-backed session ids are constrained before they become filenames: + +- allowed characters are ASCII letters, digits, `_`, and `-` +- maximum length is 128 characters +- path separators, dot-prefixed ids, whitespace, and shell metacharacters are rejected + +`FileSessionStore` is a local durable store, not a distributed database. Do not share the same store +root between unrelated users or untrusted processes. + ## DeepAgents Compatibility DeepAgents graphs are supported as compiled LangGraph targets. @@ -129,3 +249,4 @@ Docs: - - - +- diff --git a/packages/adapters/langchain-acp/VERSION b/packages/adapters/langchain-acp/VERSION index ac39a10..85b7c69 100644 --- a/packages/adapters/langchain-acp/VERSION +++ b/packages/adapters/langchain-acp/VERSION @@ -1 +1 @@ -0.9.0 +0.9.6 diff --git a/packages/adapters/langchain-acp/pyproject.toml b/packages/adapters/langchain-acp/pyproject.toml index 06845d4..78bf85e 100644 --- a/packages/adapters/langchain-acp/pyproject.toml +++ b/packages/adapters/langchain-acp/pyproject.toml @@ -18,6 +18,7 @@ dependencies = [ "agent-client-protocol>=0.9.0", "langchain>=1.0.0", "langgraph>=1.0.0", + "pydantic>=2.7", "typing-extensions>=4.12.0", ] diff --git a/packages/adapters/langchain-acp/src/langchain_acp/__init__.py b/packages/adapters/langchain-acp/src/langchain_acp/__init__.py index 08cfea5..bfd0639 100644 --- a/packages/adapters/langchain-acp/src/langchain_acp/__init__.py +++ b/packages/adapters/langchain-acp/src/langchain_acp/__init__.py @@ -1,12 +1,26 @@ from __future__ import annotations as _annotations from ._version import __version__ -from .approvals import ApprovalBridge, ApprovalDecision, NativeApprovalBridge +from .approval_store import ( + ApprovalPolicy, + ApprovalPolicyStore, + PermissionOptionSet, + SessionMetadataApprovalPolicyStore, +) +from .approvals import ( + ApprovalBridge, + ApprovalDecision, + NativeApprovalBridge, + ProjectionAwareApprovalBridge, + supports_projection_aware_approval_bridge, +) from .bridges import ( BufferedCapabilityBridge, CapabilityBridge, ConfigOptionsBridge, DeepAgentsCompatibilityBridge, + EventEmissionMode, + ExternalHookEventBridge, ModelSelectionBridge, ModeSelectionBridge, ToolSurfaceBridge, @@ -26,6 +40,12 @@ GraphSource, StaticGraphSource, ) +from .hook_projection import HookEvent, HookProjectionMap +from .permission_presentation import ( + DefaultPermissionToolCallBuilder, + PermissionRequestContext, + PermissionToolCallBuilder, +) from .plan import ( NativePlanGeneration, PlanGenerationType, @@ -46,6 +66,7 @@ FileSystemProjectionMap, FinanceProjectionMap, HttpRequestProjectionMap, + ProjectionAwareToolClassifier, ProjectionMap, ToolClassifier, WebFetchProjectionMap, @@ -55,6 +76,7 @@ compose_projection_maps, extract_tool_call_locations, ) +from .prompt_capabilities import AdapterPromptCapabilities from .providers import ( ConfigOption, ConfigOptionsProvider, @@ -73,6 +95,14 @@ MemorySessionStore, SessionStore, ) +from .slash import ( + SlashCommandHandler, + SlashCommandProvider, + SlashCommandRequest, + SlashCommandResult, + StaticSlashCommand, + StaticSlashCommandProvider, +) from .types import ( AcpAgent, AgentPromptBlock, @@ -94,8 +124,11 @@ "AcpAgent", "AcpSessionContext", "AdapterConfig", + "AdapterPromptCapabilities", "AgentPromptBlock", "ApprovalBridge", + "ApprovalPolicy", + "ApprovalPolicyStore", "CompositeEventProjectionMap", "ApprovalDecision", "AudioContentBlock", @@ -117,7 +150,9 @@ "DeepAgentsProjectionMap", "DeepAgentsCompatibilityBridge", "EmbeddedResourceContentBlock", + "EventEmissionMode", "EventProjectionMap", + "ExternalHookEventBridge", "FactoryGraphSource", "FileSessionStore", "FileSystemProjectionMap", @@ -126,6 +161,8 @@ "GraphBridgeBuilder", "GraphBuildContributions", "GraphSource", + "HookEvent", + "HookProjectionMap", "HttpRequestProjectionMap", "HttpMcpServer", "ImageContentBlock", @@ -143,14 +180,27 @@ "PlanEntry", "PlanGenerationType", "PlanProvider", + "ProjectionAwareApprovalBridge", + "ProjectionAwareToolClassifier", "ProjectionMap", + "PermissionOptionSet", + "PermissionRequestContext", + "PermissionToolCallBuilder", + "DefaultPermissionToolCallBuilder", "ResourceContentBlock", + "SessionMetadataApprovalPolicyStore", "SessionModelsProvider", "SessionModesProvider", "SessionStore", "StaticGraphSource", "StructuredEventProjectionMap", "SseMcpServer", + "SlashCommandHandler", + "SlashCommandProvider", + "SlashCommandRequest", + "SlashCommandResult", + "StaticSlashCommand", + "StaticSlashCommandProvider", "TaskPlan", "TextContentBlock", "TextResourceContents", @@ -170,4 +220,5 @@ "extract_tool_call_locations", "native_plan_tools", "run_acp", + "supports_projection_aware_approval_bridge", ) diff --git a/packages/adapters/langchain-acp/src/langchain_acp/_slash_commands.py b/packages/adapters/langchain-acp/src/langchain_acp/_slash_commands.py new file mode 100644 index 0000000..4e45eaa --- /dev/null +++ b/packages/adapters/langchain-acp/src/langchain_acp/_slash_commands.py @@ -0,0 +1,46 @@ +from __future__ import annotations as _annotations + +from collections.abc import Iterable + +__all__ = ( + "MCP_SERVERS_COMMAND_NAME", + "MODEL_COMMAND_NAME", + "RESERVED_SLASH_COMMAND_NAMES", + "TOOLS_COMMAND_NAME", + "validate_mode_command_ids", +) + +MODEL_COMMAND_NAME = "model" +TOOLS_COMMAND_NAME = "tools" +MCP_SERVERS_COMMAND_NAME = "mcp-servers" +RESERVED_SLASH_COMMAND_NAMES = frozenset( + { + MODEL_COMMAND_NAME, + TOOLS_COMMAND_NAME, + MCP_SERVERS_COMMAND_NAME, + } +) + + +def validate_mode_command_ids(mode_ids: Iterable[str]) -> None: + normalized_ids: list[str] = [] + for mode_id in mode_ids: + normalized_id = mode_id.strip().lower() + if not normalized_id: + raise ValueError("Mode slash command ids must be non-empty after normalization.") + if any(character.isspace() for character in normalized_id): + raise ValueError( + f"Mode slash command id {mode_id!r} cannot contain whitespace after normalization." + ) + normalized_ids.append(normalized_id) + duplicate_ids = sorted( + mode_id for mode_id in set(normalized_ids) if normalized_ids.count(mode_id) > 1 + ) + if duplicate_ids: + raise ValueError(f"Duplicate ids: {', '.join(duplicate_ids)}.") + reserved_ids = sorted(set(normalized_ids) & RESERVED_SLASH_COMMAND_NAMES) + if reserved_ids: + raise ValueError( + "Mode slash command ids cannot reuse reserved slash command names " + f"({', '.join(reserved_ids)})." + ) diff --git a/packages/adapters/langchain-acp/src/langchain_acp/_version.py b/packages/adapters/langchain-acp/src/langchain_acp/_version.py index bd0f56b..e39e524 100644 --- a/packages/adapters/langchain-acp/src/langchain_acp/_version.py +++ b/packages/adapters/langchain-acp/src/langchain_acp/_version.py @@ -2,4 +2,4 @@ __all__ = ("__version__",) -__version__ = "0.9.0" +__version__ = "0.9.6" diff --git a/packages/adapters/langchain-acp/src/langchain_acp/approval_store.py b/packages/adapters/langchain-acp/src/langchain_acp/approval_store.py new file mode 100644 index 0000000..2ddbcbd --- /dev/null +++ b/packages/adapters/langchain-acp/src/langchain_acp/approval_store.py @@ -0,0 +1,89 @@ +from __future__ import annotations as _annotations + +from dataclasses import dataclass +from typing import Literal, Protocol, TypeAlias + +from typing_extensions import TypeIs + +from .session.state import AcpSessionContext, JsonValue + +__all__ = ( + "ApprovalPolicy", + "ApprovalPolicyStore", + "PermissionOptionSet", + "SessionMetadataApprovalPolicyStore", +) + +ApprovalPolicy: TypeAlias = Literal["allow", "reject"] + + +def _is_approval_policy(value: JsonValue) -> TypeIs[ApprovalPolicy]: + return value in {"allow", "reject"} + + +class ApprovalPolicyStore(Protocol): + def get_policy( + self, + session: AcpSessionContext, + policy_key: str, + ) -> ApprovalPolicy | None: ... + + def set_policy( + self, + session: AcpSessionContext, + policy_key: str, + policy: ApprovalPolicy, + ) -> None: ... + + def export_state( + self, + session: AcpSessionContext, + ) -> dict[str, JsonValue] | None: ... + + +@dataclass(slots=True) +class SessionMetadataApprovalPolicyStore: + metadata_key: str = "approval_policies" + + def get_policy( + self, + session: AcpSessionContext, + policy_key: str, + ) -> ApprovalPolicy | None: + policy = self._policies(session).get(policy_key) + if _is_approval_policy(policy): + return policy + return None + + def set_policy( + self, + session: AcpSessionContext, + policy_key: str, + policy: ApprovalPolicy, + ) -> None: + raw_policies = session.metadata.get(self.metadata_key) + if not isinstance(raw_policies, dict): + raw_policies = {} + session.metadata[self.metadata_key] = raw_policies + raw_policies[policy_key] = policy + + def export_state( + self, + session: AcpSessionContext, + ) -> dict[str, JsonValue] | None: + policies = self._policies(session) + return dict(policies) if policies else None + + def _policies(self, session: AcpSessionContext) -> dict[str, JsonValue]: + raw_policies = session.metadata.get(self.metadata_key) + if isinstance(raw_policies, dict): + return raw_policies + return {} + + +@dataclass(frozen=True, slots=True, kw_only=True) +class PermissionOptionSet: + allow_once_name: str = "Allow" + reject_once_name: str = "Deny" + allow_always_name: str = "Always Allow" + reject_always_name: str = "Always Deny" diff --git a/packages/adapters/langchain-acp/src/langchain_acp/approvals.py b/packages/adapters/langchain-acp/src/langchain_acp/approvals.py index a6d493b..77be282 100644 --- a/packages/adapters/langchain-acp/src/langchain_acp/approvals.py +++ b/packages/adapters/langchain-acp/src/langchain_acp/approvals.py @@ -1,16 +1,34 @@ from __future__ import annotations as _annotations -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Any, Protocol from acp.exceptions import RequestError from acp.interfaces import Client as AcpClient -from acp.schema import PermissionOption, ToolCallUpdate +from acp.schema import PermissionOption +from typing_extensions import TypeIs -from .projection import ToolClassifier, extract_tool_call_locations +from .approval_store import ( + ApprovalPolicy, + ApprovalPolicyStore, + PermissionOptionSet, + SessionMetadataApprovalPolicyStore, +) +from .permission_presentation import ( + DefaultPermissionToolCallBuilder, + PermissionRequestContext, + PermissionToolCallBuilder, +) +from .projection import ProjectionMap, ToolClassifier from .session.state import AcpSessionContext -__all__ = ("ApprovalBridge", "ApprovalDecision", "NativeApprovalBridge") +__all__ = ( + "ApprovalBridge", + "ApprovalDecision", + "NativeApprovalBridge", + "ProjectionAwareApprovalBridge", + "supports_projection_aware_approval_bridge", +) @dataclass(slots=True, frozen=True, kw_only=True) @@ -31,8 +49,41 @@ async def resolve_action_requests( ) -> ApprovalDecision: ... +class ProjectionAwareApprovalBridge(Protocol): + async def resolve_action_requests( + self, + *, + client: AcpClient, + session: AcpSessionContext, + action_requests: list[dict[str, Any]], + review_configs: list[dict[str, Any]], + classifier: ToolClassifier, + projection_map: ProjectionMap | None, + ) -> ApprovalDecision: ... + + +def supports_projection_aware_approval_bridge( + bridge: ApprovalBridge | ProjectionAwareApprovalBridge | None, +) -> TypeIs[ProjectionAwareApprovalBridge]: + if bridge is None: + return False + return hasattr(bridge, "_supports_projection_aware_approval_bridge") + + @dataclass(slots=True, kw_only=True) class NativeApprovalBridge: + enable_persistent_choices: bool = False + option_set: PermissionOptionSet = field(default_factory=PermissionOptionSet) + policy_store: ApprovalPolicyStore = field(default_factory=SessionMetadataApprovalPolicyStore) + tool_call_builder: PermissionToolCallBuilder = field( + default_factory=DefaultPermissionToolCallBuilder + ) + _supports_projection_aware_approval_bridge: bool = field( + default=True, + init=False, + repr=False, + ) + async def resolve_action_requests( self, *, @@ -41,6 +92,7 @@ async def resolve_action_requests( action_requests: list[dict[str, Any]], review_configs: list[dict[str, Any]], classifier: ToolClassifier, + projection_map: ProjectionMap | None = None, ) -> ApprovalDecision: decisions: list[dict[str, Any]] = [] config_by_action = { @@ -55,6 +107,18 @@ async def resolve_action_requests( tool_args = action_request.get("args", {}) if not isinstance(tool_name, str) or not isinstance(tool_args, dict): raise RequestError.invalid_request({"action_request": action_request}) + policy_key = classifier.approval_policy_key(tool_name, tool_args) + remembered_policy = ( + self.policy_store.get_policy(session, policy_key) + if self.enable_persistent_choices + else None + ) + if remembered_policy == "allow": + decisions.append({"type": "approve"}) + continue + if remembered_policy == "reject": + decisions.append({"type": "reject"}) + continue review_config = config_by_action.get(tool_name, {}) allowed_decisions = review_config.get("allowed_decisions", ["approve", "reject"]) if "edit" in allowed_decisions and set(allowed_decisions) == {"edit"}: @@ -64,10 +128,16 @@ async def resolve_action_requests( permission = await client.request_permission( session_id=session.session_id, options=self._build_permission_options(), - tool_call=self._build_tool_call_update( - tool_name=tool_name, - tool_args=tool_args, - classifier=classifier, + tool_call=self.tool_call_builder.build_tool_call_update( + PermissionRequestContext( + session=session, + tool_call_id=self._tool_call_id(action_request, tool_name), + tool_name=tool_name, + raw_input=tool_args, + cwd=session.cwd, + classifier=classifier, + projection_map=projection_map, + ) ), ) outcome = permission.outcome @@ -80,27 +150,59 @@ async def resolve_action_requests( if option_id == "reject_once": decisions.append({"type": "reject"}) continue + if option_id == "allow_always": + self._remember_policy(session, policy_key, "allow") + decisions.append({"type": "approve"}) + continue + if option_id == "reject_always": + self._remember_policy(session, policy_key, "reject") + decisions.append({"type": "reject"}) + continue raise RequestError.invalid_request({"optionId": option_id}) return ApprovalDecision(decisions=decisions) def _build_permission_options(self) -> list[PermissionOption]: - return [ - PermissionOption(option_id="allow_once", name="Allow", kind="allow_once"), - PermissionOption(option_id="reject_once", name="Deny", kind="reject_once"), + options = [ + PermissionOption( + option_id="allow_once", + name=self.option_set.allow_once_name, + kind="allow_once", + ), + PermissionOption( + option_id="reject_once", + name=self.option_set.reject_once_name, + kind="reject_once", + ), ] + if self.enable_persistent_choices: + options.extend( + [ + PermissionOption( + option_id="allow_always", + name=self.option_set.allow_always_name, + kind="allow_always", + ), + PermissionOption( + option_id="reject_always", + name=self.option_set.reject_always_name, + kind="reject_always", + ), + ] + ) + return options - def _build_tool_call_update( + def _remember_policy( self, - *, - tool_name: str, - tool_args: dict[str, Any], - classifier: ToolClassifier, - ) -> ToolCallUpdate: - return ToolCallUpdate( - tool_call_id=f"hitl:{tool_name}", - title=tool_name, - kind=classifier.classify(tool_name, tool_args), - locations=extract_tool_call_locations(tool_args), - raw_input=tool_args, - status="in_progress", - ) + session: AcpSessionContext, + policy_key: str, + policy: ApprovalPolicy, + ) -> None: + if not self.enable_persistent_choices: + return + self.policy_store.set_policy(session, policy_key, policy) + + def _tool_call_id(self, action_request: dict[str, Any], tool_name: str) -> str: + action_id = action_request.get("id") + if isinstance(action_id, str) and action_id: + return action_id + return f"hitl:{tool_name}" diff --git a/packages/adapters/langchain-acp/src/langchain_acp/bridges/__init__.py b/packages/adapters/langchain-acp/src/langchain_acp/bridges/__init__.py index 11372bc..8a51619 100644 --- a/packages/adapters/langchain-acp/src/langchain_acp/bridges/__init__.py +++ b/packages/adapters/langchain-acp/src/langchain_acp/bridges/__init__.py @@ -8,12 +8,15 @@ ModeSelectionBridge, ToolSurfaceBridge, ) +from .external_hooks import EventEmissionMode, ExternalHookEventBridge __all__ = ( "BufferedCapabilityBridge", "CapabilityBridge", "ConfigOptionsBridge", "DeepAgentsCompatibilityBridge", + "EventEmissionMode", + "ExternalHookEventBridge", "ModeSelectionBridge", "ModelSelectionBridge", "ToolSurfaceBridge", diff --git a/packages/adapters/langchain-acp/src/langchain_acp/bridges/external_hooks.py b/packages/adapters/langchain-acp/src/langchain_acp/bridges/external_hooks.py new file mode 100644 index 0000000..8fdefa3 --- /dev/null +++ b/packages/adapters/langchain-acp/src/langchain_acp/bridges/external_hooks.py @@ -0,0 +1,58 @@ +from __future__ import annotations as _annotations + +from dataclasses import dataclass, field +from typing import Literal, TypeAlias + +from ..hook_projection import HookEvent, HookProjectionMap +from ..session.state import AcpSessionContext, JsonValue +from .base import BufferedCapabilityBridge + +__all__ = ("EventEmissionMode", "ExternalHookEventBridge") + +EventEmissionMode: TypeAlias = Literal["paired", "start_only"] + + +@dataclass(slots=True, kw_only=True) +class ExternalHookEventBridge(BufferedCapabilityBridge): + metadata_key: str | None = "external_hooks" + projection_map: HookProjectionMap = field(default_factory=HookProjectionMap) + emission_mode: EventEmissionMode = "paired" + + def record_event( + self, + session: AcpSessionContext, + event: HookEvent, + *, + emission_mode: EventEmissionMode | None = None, + ) -> None: + mode = self.emission_mode if emission_mode is None else emission_mode + tool_call_id = self._next_event_id(session) + start_update = self.projection_map.build_start_update( + tool_call_id=tool_call_id, + event=event, + ) + if start_update is None: + return + if mode == "start_only": + if event.status is not None: + start_update.status = event.status + self._append_updates(session, [start_update]) + return + progress_update = self.projection_map.build_progress_update( + tool_call_id=tool_call_id, + event=event, + ) + if progress_update is None: + self._append_updates(session, [start_update]) + return + self._append_updates(session, [start_update, progress_update]) + + def get_session_metadata(self, session: AcpSessionContext) -> dict[str, JsonValue]: + hidden_event_ids: list[JsonValue] = [] + hidden_event_ids.extend(sorted(self.projection_map.hidden_event_ids)) + return { + "emission_mode": self.emission_mode, + "pending_event_count": len(self._pending_updates.get(session.session_id, ())), + "hidden_event_ids": hidden_event_ids, + "projection_title_prefix": self.projection_map.title_prefix, + } diff --git a/packages/adapters/langchain-acp/src/langchain_acp/config.py b/packages/adapters/langchain-acp/src/langchain_acp/config.py index 816865d..d5b6c3f 100644 --- a/packages/adapters/langchain-acp/src/langchain_acp/config.py +++ b/packages/adapters/langchain-acp/src/langchain_acp/config.py @@ -11,6 +11,7 @@ from .event_projection import EventProjectionMap from .plan import PlanGenerationType from .projection import DefaultToolClassifier, ProjectionMap, ToolClassifier +from .prompt_capabilities import AdapterPromptCapabilities from .providers import ( ConfigOptionsProvider, NativePlanPersistenceProvider, @@ -20,6 +21,7 @@ ) from .serialization import DefaultOutputSerializer, OutputSerializer from .session.store import MemorySessionStore, SessionStore +from .slash import SlashCommandProvider DEFAULT_AGENT_NAME = "langchain-acp" DEFAULT_AGENT_TITLE = "LangChain ACP" @@ -56,6 +58,10 @@ class AdapterConfig: plan_mode_id: str | None = None plan_provider: PlanProvider | None = None projection_maps: Sequence[ProjectionMap] = field(default_factory=tuple) + prompt_capabilities: AdapterPromptCapabilities = field( + default_factory=AdapterPromptCapabilities + ) replay_history_on_load: bool = True + slash_command_provider: SlashCommandProvider | None = None session_store: SessionStore = field(default_factory=MemorySessionStore) tool_classifier: ToolClassifier = field(default_factory=DefaultToolClassifier) diff --git a/packages/adapters/langchain-acp/src/langchain_acp/hook_projection.py b/packages/adapters/langchain-acp/src/langchain_acp/hook_projection.py new file mode 100644 index 0000000..eeafb35 --- /dev/null +++ b/packages/adapters/langchain-acp/src/langchain_acp/hook_projection.py @@ -0,0 +1,78 @@ +from __future__ import annotations as _annotations + +from dataclasses import dataclass, field + +from acp.schema import ( + ContentToolCallContent, + TextContentBlock, + ToolCallProgress, + ToolCallStart, + ToolCallStatus, +) + +__all__ = ("HookEvent", "HookProjectionMap") + + +@dataclass(frozen=True, slots=True, kw_only=True) +class HookEvent: + event_id: str + hook_name: str + summary: str + detail: str | None = None + status: ToolCallStatus | None = None + hidden: bool = False + + +@dataclass(slots=True, kw_only=True) +class HookProjectionMap: + title_prefix: str = "Hook" + hidden_event_ids: set[str] = field(default_factory=set) + + def build_start_update( + self, + *, + tool_call_id: str, + event: HookEvent, + ) -> ToolCallStart | None: + if event.hidden: + self.hidden_event_ids.add(event.event_id) + return None + return ToolCallStart( + session_update="tool_call", + tool_call_id=tool_call_id, + title=f"{self.title_prefix}: {event.hook_name}", + kind="execute", + status="in_progress", + raw_input={"event_id": event.event_id, "summary": event.summary}, + content=[ + ContentToolCallContent( + type="content", + content=TextContentBlock(type="text", text=event.summary), + ) + ], + ) + + def build_progress_update( + self, + *, + tool_call_id: str, + event: HookEvent, + ) -> ToolCallProgress | None: + if event.hidden: + self.hidden_event_ids.add(event.event_id) + return None + detail = event.detail or event.summary + return ToolCallProgress( + session_update="tool_call_update", + tool_call_id=tool_call_id, + title=f"{self.title_prefix}: {event.hook_name}", + kind="execute", + status=event.status or "completed", + raw_output=detail, + content=[ + ContentToolCallContent( + type="content", + content=TextContentBlock(type="text", text=detail), + ) + ], + ) diff --git a/packages/adapters/langchain-acp/src/langchain_acp/permission_presentation.py b/packages/adapters/langchain-acp/src/langchain_acp/permission_presentation.py new file mode 100644 index 0000000..5a56de5 --- /dev/null +++ b/packages/adapters/langchain-acp/src/langchain_acp/permission_presentation.py @@ -0,0 +1,68 @@ +from __future__ import annotations as _annotations + +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Protocol + +from acp.schema import ToolCallStatus, ToolCallUpdate + +from .projection import ProjectionMap, ToolClassifier, extract_tool_call_locations +from .session.state import AcpSessionContext + +__all__ = ( + "DefaultPermissionToolCallBuilder", + "PermissionRequestContext", + "PermissionToolCallBuilder", +) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class PermissionRequestContext: + session: AcpSessionContext + tool_call_id: str + tool_name: str + raw_input: dict[str, Any] + cwd: Path + classifier: ToolClassifier + projection_map: ProjectionMap | None = None + + +class PermissionToolCallBuilder(Protocol): + def build_tool_call_update( + self, + context: PermissionRequestContext, + ) -> ToolCallUpdate: ... + + +@dataclass(frozen=True, slots=True, kw_only=True) +class DefaultPermissionToolCallBuilder: + status: ToolCallStatus = "pending" + + def build_tool_call_update( + self, + context: PermissionRequestContext, + ) -> ToolCallUpdate: + projection = None + if context.projection_map is not None: + projection = context.projection_map.project_start( + context.tool_name, + cwd=context.cwd, + raw_input=context.raw_input, + ) + return ToolCallUpdate( + tool_call_id=context.tool_call_id, + title=( + projection.title + if projection is not None and projection.title is not None + else context.tool_name + ), + kind=context.classifier.classify(context.tool_name, context.raw_input), + content=projection.content if projection is not None else None, + locations=( + projection.locations + if projection is not None and projection.locations is not None + else extract_tool_call_locations(context.raw_input) + ), + raw_input=context.raw_input, + status=self.status, + ) diff --git a/packages/adapters/langchain-acp/src/langchain_acp/projection.py b/packages/adapters/langchain-acp/src/langchain_acp/projection.py index fc3f4ca..4a1b39c 100644 --- a/packages/adapters/langchain-acp/src/langchain_acp/projection.py +++ b/packages/adapters/langchain-acp/src/langchain_acp/projection.py @@ -30,6 +30,7 @@ "FinanceProjectionMap", "HttpRequestProjectionMap", "ProjectionMap", + "ProjectionAwareToolClassifier", "ToolClassifier", "WebFetchProjectionMap", "WebSearchProjectionMap", @@ -270,6 +271,12 @@ class FileSystemProjectionMap: read_tool_names: frozenset[str] = frozenset() search_tool_names: frozenset[str] = frozenset() execute_tool_names: frozenset[str] = frozenset() + default_search_tool: str | None = None + search_path_arg: str = "path" + search_pattern_arg: str = "pattern" + render_search_results_as_tree: bool = False + hide_dot_directories_in_tree: bool = False + tree_root_label: str = "." def project_start( self, @@ -281,7 +288,7 @@ def project_start( del cwd if not _is_string_keyed_object_dict(raw_input): return None - if tool_name in self.execute_tool_names: + if tool_name in self._execute_tool_names(): command = _command_text(raw_input) if command is None: return None @@ -297,7 +304,7 @@ def project_start( content=content, title=_format_command_title(command), ) - if tool_name in self.write_tool_names: + if tool_name in self._write_tool_names(): path = _first_string(raw_input, _PATH_KEYS) new_text = _first_string(raw_input, _NEW_CONTENT_KEYS) if path is None or new_text is None: @@ -315,7 +322,7 @@ def project_start( locations=[ToolCallLocation(path=path)], title=_tool_title(tool_name, path=path), ) - if tool_name in self.read_tool_names: + if tool_name in self._read_tool_names(): path = _first_string(raw_input, _PATH_KEYS) if path is None: return None @@ -323,9 +330,9 @@ def project_start( locations=[ToolCallLocation(path=path)], title=_tool_title(tool_name, path=path), ) - if tool_name in self.search_tool_names: - location = _first_string(raw_input, _PATH_KEYS) - search_term = _first_string(raw_input, _SEARCH_KEYS) + if tool_name in self._search_tool_names(): + location = _first_string(raw_input, (self.search_path_arg, *_PATH_KEYS)) + search_term = _first_string(raw_input, (self.search_pattern_arg, *_SEARCH_KEYS)) locations = [ToolCallLocation(path=location)] if location is not None else None return ToolProjection( locations=locations, @@ -346,7 +353,7 @@ def project_progress( del cwd if status != "completed": return None - if tool_name in self.read_tool_names and _is_string_keyed_object_dict(raw_input): + if tool_name in self._read_tool_names() and _is_string_keyed_object_dict(raw_input): path = _first_string(raw_input, _PATH_KEYS) if path is None: return None @@ -362,7 +369,7 @@ def project_progress( locations=[ToolCallLocation(path=path)], title=_tool_title(tool_name, path=path), ) - if tool_name in self.execute_tool_names: + if tool_name in self._execute_tool_names(): content: list[ ContentToolCallContent | FileEditToolCallContent | TerminalToolCallContent ] = [] @@ -378,6 +385,74 @@ def project_progress( content=content or None, title=_command_title_from_input(raw_input), ) + if tool_name in self._search_tool_names() and _is_string_keyed_object_dict(raw_input): + location = _first_string(raw_input, (self.search_path_arg, *_PATH_KEYS)) + search_term = _first_string(raw_input, (self.search_pattern_arg, *_SEARCH_KEYS)) + output_text = _output_text(raw_output, serialized_output) + if self.render_search_results_as_tree: + output_text = _render_path_tree( + output_text, + root_label=location or self.tree_root_label, + hide_dot_directories=self.hide_dot_directories_in_tree, + ) + else: + output_text = _truncate_text(output_text, limit=_MAX_CONTENT_PREVIEW_CHARS) + locations = [ToolCallLocation(path=location)] if location is not None else None + return ToolProjection( + content=[ContentToolCallContent(type="content", content=_text_block(output_text))], + locations=locations, + title=_search_title(tool_name, search_term=search_term, path=location), + ) + return None + + def _read_tool_names(self) -> frozenset[str]: + return self.read_tool_names + + def _write_tool_names(self) -> frozenset[str]: + return self.write_tool_names + + def _search_tool_names(self) -> frozenset[str]: + if self.default_search_tool is None: + return self.search_tool_names + return frozenset((*self.search_tool_names, self.default_search_tool)) + + def _execute_tool_names(self) -> frozenset[str]: + return self.execute_tool_names + + +@dataclass(slots=True, frozen=True, kw_only=True) +class ProjectionAwareToolClassifier: + base_classifier: ToolClassifier + projection_maps: Sequence[ProjectionMap] + + def classify(self, tool_name: str, raw_input: Any = None) -> ToolKind: + projection_kind = self._projection_kind(tool_name) + if projection_kind is not None: + return projection_kind + return self.base_classifier.classify(tool_name, raw_input) + + def approval_policy_key(self, tool_name: str, raw_input: Any = None) -> str: + return self.base_classifier.approval_policy_key(tool_name, raw_input) + + def _projection_kind(self, tool_name: str) -> ToolKind | None: + for projection_map in self.projection_maps: + if isinstance(projection_map, FileSystemProjectionMap): + if tool_name in projection_map._read_tool_names(): + return "read" + if tool_name in projection_map._write_tool_names(): + return "edit" + if tool_name in projection_map._execute_tool_names(): + return "execute" + if tool_name in projection_map._search_tool_names(): + return "search" + if isinstance(projection_map, CompositeProjectionMap): + nested_classifier = ProjectionAwareToolClassifier( + base_classifier=self.base_classifier, + projection_maps=projection_map.maps, + ) + projection_kind = nested_classifier._projection_kind(tool_name) + if projection_kind is not None: + return projection_kind return None @@ -962,6 +1037,92 @@ def extract_tool_call_locations(raw_input: Any) -> list[ToolCallLocation]: return [ToolCallLocation(path=path)] +@dataclass(slots=True) +class _PathTreeNode: + directories: dict[str, _PathTreeNode] = field(default_factory=dict) + files: set[str] = field(default_factory=set) + + +def _render_path_tree( + text: str, + *, + root_label: str, + hide_dot_directories: bool, +) -> str: + root = _PathTreeNode() + has_path = False + truncated = False + for raw_line in text.splitlines(): + line = raw_line.strip() + if not line: + continue + if line == "...": + truncated = True + continue + normalized = line.removeprefix("./") + if not normalized: + continue + if _path_has_hidden_directory( + normalized, + hide_dot_directories=hide_dot_directories, + ): + continue + _insert_tree_path(root, normalized) + has_path = True + if not has_path: + return text + lines = [f"Tree: {root_label}", ""] + entries = _render_tree_node(root, prefix="") + lines.extend(entries) + if truncated: + lines.append("...") + return "\n".join(lines).rstrip() + + +def _path_has_hidden_directory(path: str, *, hide_dot_directories: bool) -> bool: + if not hide_dot_directories: + return False + parts = [part for part in path.strip("/").split("/") if part] + directory_parts = parts[:-1] if not path.endswith("/") else parts + return any(part.startswith(".") for part in directory_parts) + + +def _insert_tree_path(root: _PathTreeNode, path: str) -> None: + normalized = path.strip("/") + if not normalized: + return + is_directory = path.endswith("/") + parts = [part for part in normalized.split("/") if part] + node = root + last_index = len(parts) - 1 + for index, part in enumerate(parts): + is_last = index == last_index + if is_last and not is_directory: + node.files.add(part) + return + node = node.directories.setdefault(part, _PathTreeNode()) + + +def _render_tree_node(node: _PathTreeNode, *, prefix: str) -> list[str]: + lines: list[str] = [] + entries: list[tuple[str, str, _PathTreeNode | None]] = [] + for directory_name in sorted(node.directories): + entries.append(("dir", directory_name, node.directories[directory_name])) + for file_name in sorted(node.files): + entries.append(("file", file_name, None)) + for index, (entry_type, name, child) in enumerate(entries): + is_last = index == len(entries) - 1 + connector = "└── " if is_last else "├── " + if entry_type == "dir": + lines.append(f"{prefix}{connector}{name}/") + child_prefix = f"{prefix}{' ' if is_last else '│ '}" + assert child is not None + lines.extend(_render_tree_node(child, prefix=child_prefix)) + continue + lines.append(f"{prefix}{connector}{name}") + return lines + + def build_tool_start_update( *, tool_call_id: str, diff --git a/packages/adapters/langchain-acp/src/langchain_acp/prompt_capabilities.py b/packages/adapters/langchain-acp/src/langchain_acp/prompt_capabilities.py new file mode 100644 index 0000000..7b942e5 --- /dev/null +++ b/packages/adapters/langchain-acp/src/langchain_acp/prompt_capabilities.py @@ -0,0 +1,12 @@ +from __future__ import annotations as _annotations + +from dataclasses import dataclass + +__all__ = ("AdapterPromptCapabilities",) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class AdapterPromptCapabilities: + audio: bool = True + image: bool = True + embedded_context: bool = True diff --git a/packages/adapters/langchain-acp/src/langchain_acp/runtime/_native_plan_runtime.py b/packages/adapters/langchain-acp/src/langchain_acp/runtime/_native_plan_runtime.py index b7cc2b3..f914dc8 100644 --- a/packages/adapters/langchain-acp/src/langchain_acp/runtime/_native_plan_runtime.py +++ b/packages/adapters/langchain-acp/src/langchain_acp/runtime/_native_plan_runtime.py @@ -83,7 +83,7 @@ async def config_options(self, session: AcpSessionContext) -> list[ConfigOption] options=[ SessionConfigSelectOption( value=value, - name="Tool-Based" if value == "tools" else "Structured", + name="Tool Plans" if value == "tools" else "Structured Plans", ) for value in _PLAN_GENERATION_CONFIG_OPTIONS ], diff --git a/packages/adapters/langchain-acp/src/langchain_acp/runtime/adapter.py b/packages/adapters/langchain-acp/src/langchain_acp/runtime/adapter.py index d7c23fc..63e0037 100644 --- a/packages/adapters/langchain-acp/src/langchain_acp/runtime/adapter.py +++ b/packages/adapters/langchain-acp/src/langchain_acp/runtime/adapter.py @@ -14,7 +14,10 @@ AgentCapabilities, AgentMessageChunk, AgentPlanUpdate, + AvailableCommandsUpdate, CloseSessionResponse, + ConfigOptionUpdate, + CurrentModeUpdate, ForkSessionResponse, HttpMcpServer, Implementation, @@ -53,7 +56,7 @@ from langgraph.types import Command from pydantic import BaseModel, ValidationError -from ..approvals import ApprovalBridge +from ..approvals import ApprovalBridge, supports_projection_aware_approval_bridge from ..bridge_manager import BridgeManager from ..builders import GraphBridgeBuilder from ..config import AdapterConfig @@ -70,9 +73,23 @@ from ..providers import ConfigOption, ModelSelectionState, ModeState from ..session.state import AcpSessionContext, JsonValue, StoredSessionUpdate, utc_now from ..session.store import SessionStore +from ..slash import SlashCommandRequest, SlashCommandResult from ..types import AgentPromptBlock from ._native_plan_runtime import _NativePlanRuntime from ._prompt_conversion import message_text, prompt_to_langchain_content +from .slash_commands import ( + MCP_SERVERS_COMMAND_NAME, + MODEL_COMMAND_NAME, + TOOLS_COMMAND_NAME, + build_available_commands, + extract_session_mcp_servers, + list_graph_tools, + parse_slash_command, + render_mcp_server_listing, + render_mode_message, + render_model_message, + render_tool_listing, +) __all__ = ("LangChainAcpAgent",) @@ -113,9 +130,9 @@ async def initialize( load_session=True, mcp_capabilities=McpCapabilities(http=True, sse=True), prompt_capabilities=PromptCapabilities( - audio=True, - embedded_context=True, - image=True, + audio=self._config.prompt_capabilities.audio, + embedded_context=self._config.prompt_capabilities.embedded_context, + image=self._config.prompt_capabilities.image, ), session_capabilities=SessionCapabilities( close=SessionCloseCapabilities(), @@ -149,6 +166,7 @@ async def new_session( session.session_mode_id = await self._initial_mode_id(session) self._sync_bridge_metadata(session) self._store.save(session) + await self._emit_available_commands(session=session) return NewSessionResponse( session_id=session_id, config_options=await self._config_options(session), @@ -172,6 +190,7 @@ async def load_session( self._sync_bridge_metadata(session) await self._replay_transcript(session) await self._drain_bridge_updates(client=self._client, session=session) + await self._emit_available_commands(session=session) session.updated_at = utc_now() self._store.save(session) return LoadSessionResponse( @@ -291,6 +310,14 @@ async def prompt( self._sync_bridge_metadata(session) graph = await self._graph_source.get_graph(session) graph = self._ensure_checkpointer(graph) + slash_response = await self._maybe_handle_slash_prompt( + session=session, + graph=graph, + prompt=prompt, + acknowledged_message_id=message_id, + ) + if slash_response is not None: + return slash_response await self._drain_bridge_updates(client=client, session=session) await self._emit_user_prompt( client=client, session=session, prompt=prompt, message_id=message_id @@ -378,6 +405,7 @@ async def fork_session( forked.mcp_servers = self._serialize_mcp_servers(mcp_servers) self._sync_bridge_metadata(forked) self._store.save(forked) + await self._emit_available_commands(session=forked) return ForkSessionResponse( session_id=new_session_id, config_options=await self._config_options(forked), @@ -399,6 +427,7 @@ async def resume_session( self._sync_bridge_metadata(session) await self._replay_transcript(session) await self._drain_bridge_updates(client=self._client, session=session) + await self._emit_available_commands(session=session) session.updated_at = utc_now() self._store.save(session) return ResumeSessionResponse( @@ -431,6 +460,192 @@ async def ext_notification(self, method: str, params: dict[str, Any]) -> None: def on_connect(self, conn: AcpClient) -> None: self._client = conn + async def _maybe_handle_slash_prompt( + self, + *, + session: AcpSessionContext, + graph: Any, + prompt: list[AgentPromptBlock], + acknowledged_message_id: str | None, + ) -> PromptResponse | None: + prompt_text = self._prompt_text(prompt) + if prompt_text is None: + return None + slash_command = parse_slash_command(prompt_text) + if slash_command is None: + return None + builtin_text = await self._handle_builtin_slash_command( + session=session, + graph=graph, + command_name=slash_command.name, + argument=slash_command.argument, + ) + if builtin_text is not None: + await self._emit_agent_text( + client=self._require_client(), + session=session, + text=builtin_text, + message_id=acknowledged_message_id, + ) + await self._emit_available_commands(session=session, graph=graph) + return PromptResponse( + stop_reason="end_turn", + user_message_id=acknowledged_message_id, + ) + slash_provider = self._config.slash_command_provider + if slash_provider is None: + return None + result = await self._await_maybe( + slash_provider.handle_command( + SlashCommandRequest( + name=slash_command.name, + argument=slash_command.argument, + raw_prompt=prompt_text, + session=session, + graph=graph, + ) + ) + ) + if result is None or not result.handled: + return None + return await self._emit_custom_slash_command_response( + session=session, + graph=graph, + result=result, + acknowledged_message_id=acknowledged_message_id, + ) + + def _prompt_text(self, prompt: list[AgentPromptBlock]) -> str | None: + parts: list[str] = [] + for block in prompt: + if isinstance(block, TextContentBlock): + parts.append(block.text) + if not parts: + return None + return "\n".join(parts) + + async def _handle_builtin_slash_command( + self, + *, + session: AcpSessionContext, + graph: Any, + command_name: str, + argument: str | None, + ) -> str | None: + mode_state = await self._mode_state(session) + if mode_state is not None: + for mode in mode_state.available_modes: + if mode.id.strip().lower() != command_name: + continue + resolved_mode = await self._set_mode(session, mode.id) + if resolved_mode is None: + return "Mode is unavailable or invalid" + current_mode_id = resolved_mode.current_mode_id + if current_mode_id is None: + return "Mode is unavailable or invalid" + await self._emit_update( + client=self._require_client(), + session=session, + update=CurrentModeUpdate( + current_mode_id=current_mode_id, + session_update="current_mode_update", + ), + ) + await self._emit_update( + client=self._require_client(), + session=session, + update=ConfigOptionUpdate( + session_update="config_option_update", + config_options=await self._config_options(session), + ), + ) + return render_mode_message(current_mode_id) + if command_name == MODEL_COMMAND_NAME: + if argument is None: + return render_model_message(session.session_model_id) + resolved_model = await self._set_model(session, argument) + if resolved_model is None: + return "Model is unavailable or invalid" + await self._emit_update( + client=self._require_client(), + session=session, + update=ConfigOptionUpdate( + session_update="config_option_update", + config_options=await self._config_options(session), + ), + ) + return render_model_message(resolved_model.current_model_id) + if command_name == TOOLS_COMMAND_NAME: + return render_tool_listing(list_graph_tools(graph)) + if command_name == MCP_SERVERS_COMMAND_NAME: + return render_mcp_server_listing(extract_session_mcp_servers(session)) + return None + + async def _emit_custom_slash_command_response( + self, + *, + session: AcpSessionContext, + graph: Any, + result: SlashCommandResult, + acknowledged_message_id: str | None, + ) -> PromptResponse: + client = self._require_client() + if result.text is not None: + await self._emit_agent_text( + client=client, + session=session, + text=result.text, + message_id=acknowledged_message_id, + ) + for update in result.updates: + await self._emit_update(client=client, session=session, update=update) + if result.refresh_session_surface: + await self._emit_available_commands(session=session, graph=graph) + return PromptResponse( + stop_reason=result.stop_reason, + user_message_id=acknowledged_message_id, + ) + + async def _emit_available_commands( + self, + *, + session: AcpSessionContext, + graph: Any | None = None, + ) -> None: + client = self._client + if client is None: + return + active_graph = graph + if active_graph is None: + active_graph = self._ensure_checkpointer(await self._graph_source.get_graph(session)) + available_commands = await self._available_commands(session=session, graph=active_graph) + await client.session_update( + session_id=session.session_id, + update=AvailableCommandsUpdate( + session_update="available_commands_update", + available_commands=available_commands, + ), + ) + + async def _available_commands( + self, + *, + session: AcpSessionContext, + graph: Any, + ) -> list[Any]: + mode_state = await self._mode_state(session) + model_state = await self._model_state(session) + custom_commands = None + if self._config.slash_command_provider is not None: + custom_commands = await self._await_maybe( + self._config.slash_command_provider.available_commands(session, graph) + ) + return build_available_commands( + mode_state=mode_state, + model_state=model_state, + custom_commands=custom_commands, + ) + async def _emit_user_prompt( self, *, @@ -652,13 +867,23 @@ async def _resolve_interrupts( review_configs = interrupt_value.get("review_configs") if not isinstance(action_requests, list) or not isinstance(review_configs, list): raise RequestError.invalid_request({"interrupt_value": interrupt_value}) - resolution = await self._approval_bridge.resolve_action_requests( - client=client, - session=session, - action_requests=cast(list[dict[str, Any]], action_requests), - review_configs=cast(list[dict[str, Any]], review_configs), - classifier=self._tool_classifier, - ) + if supports_projection_aware_approval_bridge(self._approval_bridge): + resolution = await self._approval_bridge.resolve_action_requests( + client=client, + session=session, + action_requests=cast(list[dict[str, Any]], action_requests), + review_configs=cast(list[dict[str, Any]], review_configs), + classifier=self._tool_classifier, + projection_map=self._projection_map, + ) + else: + resolution = await self._approval_bridge.resolve_action_requests( + client=client, + session=session, + action_requests=cast(list[dict[str, Any]], action_requests), + review_configs=cast(list[dict[str, Any]], review_configs), + classifier=self._tool_classifier, + ) if resolution.cancelled: raise RequestError.invalid_request({"reason": "Prompt cancelled during approval."}) decisions.extend(resolution.decisions) diff --git a/packages/adapters/langchain-acp/src/langchain_acp/runtime/server.py b/packages/adapters/langchain-acp/src/langchain_acp/runtime/server.py index f7ddfa2..962252a 100644 --- a/packages/adapters/langchain-acp/src/langchain_acp/runtime/server.py +++ b/packages/adapters/langchain-acp/src/langchain_acp/runtime/server.py @@ -1,6 +1,7 @@ from __future__ import annotations as _annotations import asyncio +from dataclasses import replace from typing import Any from acp import run_agent @@ -86,63 +87,17 @@ def _resolve_config( ) -> AdapterConfig: resolved_config = config or AdapterConfig() if projection_maps is not None or event_projection_maps is not None: - resolved_config = AdapterConfig( - agent_name=resolved_config.agent_name, - agent_title=resolved_config.agent_title, - agent_version=resolved_config.agent_version, - approval_bridge=resolved_config.approval_bridge, - available_models=list(resolved_config.available_models), - available_modes=list(resolved_config.available_modes), - capability_bridges=tuple(resolved_config.capability_bridges), - config_options_provider=resolved_config.config_options_provider, - default_model_id=resolved_config.default_model_id, - default_mode_id=resolved_config.default_mode_id, - default_plan_generation_type=resolved_config.default_plan_generation_type, - enable_plan_progress_tools=resolved_config.enable_plan_progress_tools, + resolved_config = replace( + resolved_config, event_projection_maps=tuple( event_projection_maps if event_projection_maps is not None else resolved_config.event_projection_maps ), - models_provider=resolved_config.models_provider, - modes_provider=resolved_config.modes_provider, - native_plan_additional_instructions=resolved_config.native_plan_additional_instructions, - native_plan_persistence_provider=resolved_config.native_plan_persistence_provider, - output_serializer=resolved_config.output_serializer, - plan_mode_id=resolved_config.plan_mode_id, - plan_provider=resolved_config.plan_provider, projection_maps=tuple( projection_maps if projection_maps is not None else resolved_config.projection_maps ), - replay_history_on_load=resolved_config.replay_history_on_load, - session_store=resolved_config.session_store, - tool_classifier=resolved_config.tool_classifier, ) if graph_name is None or resolved_config.agent_name != DEFAULT_AGENT_NAME: return resolved_config - return AdapterConfig( - agent_name=graph_name, - agent_title=resolved_config.agent_title, - agent_version=resolved_config.agent_version, - approval_bridge=resolved_config.approval_bridge, - available_models=list(resolved_config.available_models), - available_modes=list(resolved_config.available_modes), - capability_bridges=tuple(resolved_config.capability_bridges), - config_options_provider=resolved_config.config_options_provider, - default_model_id=resolved_config.default_model_id, - default_mode_id=resolved_config.default_mode_id, - default_plan_generation_type=resolved_config.default_plan_generation_type, - enable_plan_progress_tools=resolved_config.enable_plan_progress_tools, - event_projection_maps=tuple(resolved_config.event_projection_maps), - models_provider=resolved_config.models_provider, - modes_provider=resolved_config.modes_provider, - native_plan_additional_instructions=resolved_config.native_plan_additional_instructions, - native_plan_persistence_provider=resolved_config.native_plan_persistence_provider, - output_serializer=resolved_config.output_serializer, - plan_mode_id=resolved_config.plan_mode_id, - plan_provider=resolved_config.plan_provider, - projection_maps=tuple(resolved_config.projection_maps), - replay_history_on_load=resolved_config.replay_history_on_load, - session_store=resolved_config.session_store, - tool_classifier=resolved_config.tool_classifier, - ) + return replace(resolved_config, agent_name=graph_name) diff --git a/packages/adapters/langchain-acp/src/langchain_acp/runtime/slash_commands.py b/packages/adapters/langchain-acp/src/langchain_acp/runtime/slash_commands.py new file mode 100644 index 0000000..fc929d9 --- /dev/null +++ b/packages/adapters/langchain-acp/src/langchain_acp/runtime/slash_commands.py @@ -0,0 +1,327 @@ +from __future__ import annotations as _annotations + +import re +from collections.abc import Sequence +from dataclasses import dataclass +from typing import Any + +from acp.schema import ( + AvailableCommand, + AvailableCommandInput, + SessionMode, + SessionModelState, + SessionModeState, + UnstructuredCommandInput, +) + +from .._slash_commands import ( + MCP_SERVERS_COMMAND_NAME, + MODEL_COMMAND_NAME, + RESERVED_SLASH_COMMAND_NAMES, + TOOLS_COMMAND_NAME, + validate_mode_command_ids, +) +from ..session.state import AcpSessionContext, JsonValue + +__all__ = ( + "McpServerInfo", + "SlashCommand", + "ToolInfo", + "build_available_commands", + "extract_session_mcp_servers", + "list_graph_tools", + "parse_slash_command", + "render_mcp_server_listing", + "render_mode_message", + "render_model_message", + "render_tool_listing", + "validate_custom_commands", +) + +_COMMAND_NAME_PATTERN = re.compile(r"^[a-z][a-z0-9-]*$") + + +@dataclass(slots=True, frozen=True, kw_only=True) +class SlashCommand: + name: str + argument: str | None = None + + +@dataclass(slots=True, frozen=True, kw_only=True) +class ToolInfo: + name: str + description: str | None + + +@dataclass(slots=True, frozen=True, kw_only=True) +class McpServerInfo: + name: str + transport: str + target: str + source: str + + +def build_available_commands( + *, + mode_state: SessionModeState | None, + model_state: SessionModelState | None, + custom_commands: Sequence[AvailableCommand] | None = None, +) -> list[AvailableCommand]: + commands: list[AvailableCommand] = [] + if mode_state is not None: + validate_mode_command_ids(mode.id for mode in mode_state.available_modes) + commands.extend(_mode_commands(mode_state.available_modes)) + if model_state is not None: + commands.append( + AvailableCommand( + name=MODEL_COMMAND_NAME, + description="Show the current session model, or set it with a provider:model value.", + input=AvailableCommandInput(root=UnstructuredCommandInput(hint="provider:model")), + ) + ) + commands.extend( + [ + AvailableCommand( + name=TOOLS_COMMAND_NAME, + description="List the tools currently registered on the active graph.", + ), + AvailableCommand( + name=MCP_SERVERS_COMMAND_NAME, + description="List MCP servers attached to the current session.", + ), + ] + ) + if custom_commands: + validate_custom_commands(custom_commands, mode_state=mode_state) + commands.extend(custom_commands) + return commands + + +def validate_custom_commands( + commands: Sequence[AvailableCommand], + *, + mode_state: SessionModeState | None, +) -> None: + normalized_names: list[str] = [] + for command in commands: + normalized_name = command.name.strip().lower() + if command.name != normalized_name: + raise ValueError( + f"Slash command name {command.name!r} must already be normalized as " + "a lowercase slash command id." + ) + if not _COMMAND_NAME_PATTERN.fullmatch(normalized_name): + raise ValueError(f"Slash command name {command.name!r} must match ^[a-z][a-z0-9-]*$.") + normalized_names.append(normalized_name) + duplicate_names = sorted( + name for name in set(normalized_names) if normalized_names.count(name) > 1 + ) + if duplicate_names: + raise ValueError( + "Custom slash command names must be unique after normalization. " + f"Duplicate ids: {', '.join(duplicate_names)}." + ) + reserved_names = sorted(set(normalized_names) & RESERVED_SLASH_COMMAND_NAMES) + if reserved_names: + raise ValueError( + "Custom slash command names cannot reuse reserved slash command names " + f"({', '.join(reserved_names)})." + ) + if mode_state is None: + return + mode_ids = {mode.id.strip().lower() for mode in mode_state.available_modes} + conflicting_mode_ids = sorted(set(normalized_names) & mode_ids) + if conflicting_mode_ids: + raise ValueError( + "Custom slash command names cannot reuse active mode ids " + f"({', '.join(conflicting_mode_ids)})." + ) + + +def parse_slash_command(prompt_text: str) -> SlashCommand | None: + stripped = prompt_text.strip() + if not stripped.startswith("/"): + return None + command_text = stripped[1:] + if not command_text.strip(): + return None + name, _, remainder = command_text.partition(" ") + normalized_name = name.strip().lower() + if not normalized_name: + return None + argument = remainder.strip() or None + return SlashCommand(name=normalized_name, argument=argument) + + +def render_mode_message(current_mode_id: str | None) -> str: + if current_mode_id is None: + return "Current mode: unavailable" + return f"Current mode: {current_mode_id}" + + +def render_model_message(current_model_id: str | None) -> str: + if current_model_id is None: + return "Current model: unavailable" + return f"Current model: {current_model_id}" + + +def render_tool_listing(tool_infos: list[ToolInfo]) -> str: + if not tool_infos: + return "No tools are currently registered." + lines = ["Available tools:"] + for tool_info in tool_infos: + if tool_info.description is not None: + lines.append(f"- {tool_info.name}: {tool_info.description}") + else: + lines.append(f"- {tool_info.name}") + return "\n".join(lines) + + +def render_mcp_server_listing(server_infos: list[McpServerInfo]) -> str: + if not server_infos: + return "No MCP servers are currently attached." + lines = ["MCP servers:"] + for server_info in server_infos: + lines.append( + f"- {server_info.name} ({server_info.transport}, {server_info.source}): " + f"{server_info.target}" + ) + return "\n".join(lines) + + +def list_graph_tools(graph: Any) -> list[ToolInfo]: + graph_view_factory = getattr(graph, "get_graph", None) + if not callable(graph_view_factory): + return [] + graph_view = graph_view_factory() + nodes = getattr(graph_view, "nodes", None) + if not isinstance(nodes, dict): + return [] + tool_infos: list[ToolInfo] = [] + for node in nodes.values(): + tool_node = getattr(node, "data", None) + tools_by_name = getattr(tool_node, "_tools_by_name", None) + if not isinstance(tools_by_name, dict): + continue + string_items = [ + (name, tool) for name, tool in tools_by_name.items() if isinstance(name, str) + ] + for name, tool in sorted(string_items, key=lambda item: item[0]): + description = getattr(tool, "description", None) + tool_infos.append( + ToolInfo( + name=name, + description=description if isinstance(description, str) else None, + ) + ) + return tool_infos + + +def extract_session_mcp_servers(session: AcpSessionContext) -> list[McpServerInfo]: + server_infos: list[McpServerInfo] = [] + seen: set[tuple[str, str, str]] = set() + for raw_server in session.mcp_servers: + server_info = _mcp_server_info_from_session_payload(raw_server) + if server_info is None: + continue + dedupe_key = ( + server_info.name, + server_info.transport, + server_info.target, + ) + if dedupe_key in seen: + continue + seen.add(dedupe_key) + server_infos.append(server_info) + metadata_servers = session.metadata.get("mcp") + if not isinstance(metadata_servers, dict): + return server_infos + raw_servers = metadata_servers.get("servers") + if not isinstance(raw_servers, list): + return server_infos + for raw_server in raw_servers: + server_info = _mcp_server_info_from_bridge_metadata(raw_server) + if server_info is None: + continue + dedupe_key = ( + server_info.name, + server_info.transport, + server_info.target, + ) + if dedupe_key in seen: + continue + seen.add(dedupe_key) + server_infos.append(server_info) + return server_infos + + +def _mode_commands(modes: Sequence[SessionMode]) -> list[AvailableCommand]: + return [ + AvailableCommand( + name=mode.id, + description=mode.description or f"Switch the active session into {mode.name} mode.", + ) + for mode in modes + ] + + +def _mcp_server_info_from_session_payload( + raw_server: dict[str, JsonValue], +) -> McpServerInfo | None: + name = raw_server.get("name") + if not isinstance(name, str) or not name: + return None + transport = raw_server.get("type") + if not isinstance(transport, str) or not transport: + transport = raw_server.get("transport") + if not isinstance(transport, str) or not transport: + return None + if transport == "stdio": + command = raw_server.get("command") + args = raw_server.get("args") + rendered_args = ( + " ".join(item for item in args if isinstance(item, str)) + if isinstance(args, list) + else "" + ) + target = command if isinstance(command, str) else "" + if rendered_args: + target = f"{target} {rendered_args}" + else: + url = raw_server.get("url") + target = url if isinstance(url, str) and url else f"<{transport}>" + return McpServerInfo( + name=name, + transport=transport, + target=target, + source="session", + ) + + +def _mcp_server_info_from_bridge_metadata(raw_server: JsonValue) -> McpServerInfo | None: + raw_server_dict = _string_key_dict(raw_server) + if raw_server_dict is None: + return None + name = raw_server_dict.get("name") + transport = raw_server_dict.get("transport") + if not isinstance(name, str) or not isinstance(transport, str): + return None + url = raw_server_dict.get("url") + description = raw_server_dict.get("description") + target_parts = [value for value in (url, description) if isinstance(value, str) and value] + target = " | ".join(target_parts) if target_parts else f"<{transport}>" + return McpServerInfo( + name=name, + transport=transport, + target=target, + source="bridge", + ) + + +def _string_key_dict(value: JsonValue) -> dict[str, JsonValue] | None: + if not isinstance(value, dict): + return None + string_key_items = [(key, item) for key, item in value.items() if isinstance(key, str)] + if len(string_key_items) != len(value): + return None + return dict(string_key_items) diff --git a/packages/adapters/langchain-acp/src/langchain_acp/session/store.py b/packages/adapters/langchain-acp/src/langchain_acp/session/store.py index d075d9a..f88db97 100644 --- a/packages/adapters/langchain-acp/src/langchain_acp/session/store.py +++ b/packages/adapters/langchain-acp/src/langchain_acp/session/store.py @@ -22,6 +22,10 @@ __all__ = ("FileSessionStore", "MemorySessionStore", "SessionStore") +_MAX_SESSION_ID_LENGTH = 128 +_SESSION_ID_SAFE_CHARS = frozenset( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-" +) _STORE_LOCKS: dict[str, threading.RLock] = {} _STORE_LOCKS_GUARD = threading.Lock() @@ -79,6 +83,7 @@ class FileSessionStore: _lock_path: Path = field(init=False, repr=False) def __post_init__(self) -> None: + self.root = self.root.expanduser().resolve() self.root.mkdir(parents=True, exist_ok=True) self._process_lock = _store_lock(self.root) self._lock_path = self.root / ".acpkit-session-store.lock" @@ -111,6 +116,10 @@ def list_sessions(self) -> list[AcpSessionContext]: with self._locked(): sessions: list[AcpSessionContext] = [] for path in sorted(self.root.glob("*.json")): + try: + _validate_session_id(path.stem) + except ValueError: + continue session = self._load_session_unlocked(path.stem) if session is not None: sessions.append(session) @@ -182,10 +191,12 @@ def _parse_datetime(self, value: str) -> datetime: return datetime.fromisoformat(value) def _session_path(self, session_id: str) -> Path: - return self.root / f"{session_id}.json" + safe_session_id = _validate_session_id(session_id) + return _store_child_path(self.root, f"{safe_session_id}.json") def _temp_session_path(self, session_id: str) -> Path: - return self.root / f".acpkit-session-{session_id}-{uuid4().hex}.tmp" + safe_session_id = _validate_session_id(session_id) + return _store_child_path(self.root, f".acpkit-session-{safe_session_id}-{uuid4().hex}.tmp") def _cleanup_stale_temp_files(self) -> None: for path in self.root.glob(".acpkit-session-*.tmp"): @@ -246,3 +257,21 @@ def _store_lock(root: Path) -> threading.RLock: lock = threading.RLock() _STORE_LOCKS[key] = lock return lock + + +def _validate_session_id(session_id: str) -> str: + if not session_id: + raise ValueError("session_id must not be empty") + if len(session_id) > _MAX_SESSION_ID_LENGTH: + raise ValueError(f"session_id must be at most {_MAX_SESSION_ID_LENGTH} characters") + if any(char not in _SESSION_ID_SAFE_CHARS for char in session_id): + raise ValueError("session_id may only contain ASCII letters, digits, '_' and '-'") + return session_id + + +def _store_child_path(root: Path, filename: str) -> Path: + resolved_root = root.resolve() + path = (resolved_root / filename).resolve() + if path.parent != resolved_root: + raise ValueError("session path must stay inside FileSessionStore root") + return path diff --git a/packages/adapters/langchain-acp/src/langchain_acp/slash.py b/packages/adapters/langchain-acp/src/langchain_acp/slash.py new file mode 100644 index 0000000..e4f0eb9 --- /dev/null +++ b/packages/adapters/langchain-acp/src/langchain_acp/slash.py @@ -0,0 +1,83 @@ +from __future__ import annotations as _annotations + +from collections.abc import Awaitable, Callable, Sequence +from dataclasses import dataclass +from typing import Any, Protocol, TypeAlias + +from acp.schema import AvailableCommand, StopReason + +from .session.state import AcpSessionContext, SessionTranscriptUpdate + +__all__ = ( + "SlashCommandHandler", + "SlashCommandProvider", + "SlashCommandRequest", + "SlashCommandResult", + "StaticSlashCommand", + "StaticSlashCommandProvider", +) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class SlashCommandRequest: + name: str + argument: str | None + raw_prompt: str + session: AcpSessionContext + graph: Any + + +@dataclass(frozen=True, slots=True, kw_only=True) +class SlashCommandResult: + text: str | None = None + updates: Sequence[SessionTranscriptUpdate] = () + stop_reason: StopReason = "end_turn" + handled: bool = True + refresh_session_surface: bool = True + + +class SlashCommandProvider(Protocol): + def available_commands( + self, + session: AcpSessionContext, + graph: Any, + ) -> Sequence[AvailableCommand] | Awaitable[Sequence[AvailableCommand]]: ... + + def handle_command( + self, + request: SlashCommandRequest, + ) -> SlashCommandResult | None | Awaitable[SlashCommandResult | None]: ... + + +SlashCommandHandler: TypeAlias = Callable[ + [SlashCommandRequest], + SlashCommandResult | None | Awaitable[SlashCommandResult | None], +] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class StaticSlashCommand: + command: AvailableCommand + handler: SlashCommandHandler + + +@dataclass(frozen=True, slots=True, kw_only=True) +class StaticSlashCommandProvider: + commands: Sequence[StaticSlashCommand] + + def available_commands( + self, + session: AcpSessionContext, + graph: Any, + ) -> Sequence[AvailableCommand]: + del session, graph + return [command.command for command in self.commands] + + def handle_command( + self, + request: SlashCommandRequest, + ) -> SlashCommandResult | None | Awaitable[SlashCommandResult | None]: + for command in self.commands: + if command.command.name.strip().lower() == request.name: + return command.handler(request) + return None diff --git a/packages/adapters/pydantic-acp/README.md b/packages/adapters/pydantic-acp/README.md index 9e2c63f..1b53980 100644 --- a/packages/adapters/pydantic-acp/README.md +++ b/packages/adapters/pydantic-acp/README.md @@ -60,6 +60,42 @@ acp_agent = create_acp_agent( run_agent(acp_agent) ``` +If you are using Codex-backed Pydantic models through `codex-auth-helper`, pass explicit +instructions when building the model. That is the preferred seam for Codex-specific system behavior: + +```python +from codex_auth_helper import create_codex_responses_model +from pydantic_ai import Agent + +model = create_codex_responses_model( + "gpt-5.4", + instructions="You are a careful coding assistant.", +) +agent = Agent(model, name="codex-agent") +``` + +On the Pydantic path, `Agent(instructions=...)` is also valid and may still be useful for +agent-specific behavior layered on top of the model: + +```python +from codex_auth_helper import create_codex_responses_model +from pydantic_ai import Agent + +model = create_codex_responses_model( + "gpt-5.4", + instructions="You are a careful coding assistant.", +) +agent = Agent( + model, + name="codex-agent", + instructions="Ask for clarification when the task is underspecified.", +) +``` + +In short: Codex-backed Pydantic models should not rely on an implicit default instruction string. +Set instructions explicitly at the factory level, and add `Agent(instructions=...)` when you want +extra agent-owned behavior. + ## Native Plan Mode `TaskPlan` is the structured native plan output surface. @@ -201,6 +237,21 @@ run_acp( Use `AgentSource` when the agent and its dependencies should be built separately. Use providers when models, modes, config values, plans, or approvals belong to the host layer instead of the adapter. +## Session Store Notes + +Use `MemorySessionStore` for ephemeral local runs and `FileSessionStore` when ACP sessions should +survive process restarts. `FileSessionStore` is a local durable store, not a distributed coordination +layer. + +File-backed session ids are constrained before they become filenames: + +- allowed characters are ASCII letters, digits, `_`, and `-` +- maximum length is 128 characters +- path separators, dot-prefixed ids, whitespace, and shell metacharacters are rejected + +The file store writes through a temp file, `fsync`, and atomic replace. Malformed or partially +written session files are skipped by public load/list flows. + ## Maintained Examples Maintained runnable examples: @@ -222,11 +273,12 @@ Focused docs recipes: - [Session State and Lifecycle](https://vcoderun.github.io/acpkit/pydantic-acp/session-state/) - [Bridges](https://vcoderun.github.io/acpkit/bridges/) - [Providers](https://vcoderun.github.io/acpkit/providers/) +- [Security Guidance](https://vcoderun.github.io/acpkit/security/) - [Host Backends and Projections](https://vcoderun.github.io/acpkit/host-backends/) - [API Reference](https://vcoderun.github.io/acpkit/api/pydantic_acp/) ## Compatibility Policy -`pydantic-acp` currently pins `pydantic-ai-slim==1.83.0`. +`pydantic-acp` currently pins `pydantic-ai-slim==1.92.0`. -That pin is deliberate. The adapter is tested against a specific Pydantic AI surface and should still be upgraded deliberately, but the hook-compatibility seam is now isolated behind ACP Kit’s own compatibility layer instead of scattering private upstream imports through the runtime. +That pin is deliberate. The adapter is tested against a specific Pydantic AI surface and should still be upgraded deliberately, but the hook-compatibility seam is isolated behind ACP Kit’s own compatibility layer instead of scattering private upstream imports through the runtime. The 1.92 surface includes function-tool preparation, output-tool preparation, output validation/processing hooks, deferred-tool-call hooks, run metadata, and conversation IDs. diff --git a/packages/adapters/pydantic-acp/VERSION b/packages/adapters/pydantic-acp/VERSION index ac39a10..85b7c69 100644 --- a/packages/adapters/pydantic-acp/VERSION +++ b/packages/adapters/pydantic-acp/VERSION @@ -1 +1 @@ -0.9.0 +0.9.6 diff --git a/packages/adapters/pydantic-acp/pyproject.toml b/packages/adapters/pydantic-acp/pyproject.toml index 1fbea13..d7ee71a 100644 --- a/packages/adapters/pydantic-acp/pyproject.toml +++ b/packages/adapters/pydantic-acp/pyproject.toml @@ -8,8 +8,9 @@ license = { text = "Apache 2.0" } dependencies = [ # fixed for compliance with current API "agent-client-protocol==0.9.0", + "anyio>=4.0.0", # fixed for compliance with current API - "pydantic-ai-slim==1.83.0", + "pydantic-ai-slim==1.92.0", "pydantic>=2.7", "typing-extensions>=4.12.0", ] diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/__init__.py b/packages/adapters/pydantic-acp/src/pydantic_acp/__init__.py index c1eaa4e..3827c7a 100644 --- a/packages/adapters/pydantic-acp/src/pydantic_acp/__init__.py +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/__init__.py @@ -3,11 +3,24 @@ from ._version import __version__ from .agent_source import AgentFactory, AgentSource, FactoryAgentSource, StaticAgentSource from .agent_types import RuntimeAgent -from .approvals import ApprovalBridge, NativeApprovalBridge +from .approval_store import ( + ApprovalPolicy, + ApprovalPolicyStore, + PermissionOptionSet, + SessionMetadataApprovalPolicyStore, +) +from .approvals import ( + ApprovalBridge, + NativeApprovalBridge, + ProjectionAwareApprovalBridge, + supports_projection_aware_approval_bridge, +) from .bridges import ( AnthropicCompactionBridge, BufferedCapabilityBridge, CapabilityBridge, + EventEmissionMode, + ExternalHookEventBridge, HistoryProcessorBridge, HistoryProcessorCallable, HistoryProcessorContextual, @@ -24,6 +37,8 @@ OpenAICompactionBridge, PlanGenerationType, PrefixToolsBridge, + PrepareOutputToolsBridge, + PrepareOutputToolsMode, PrepareToolsBridge, PrepareToolsMode, SetToolMetadataBridge, @@ -49,10 +64,16 @@ TerminalBackend, ) from .models import AdapterModel +from .permission_presentation import ( + DefaultPermissionToolCallBuilder, + PermissionRequestContext, + PermissionToolCallBuilder, +) from .projection import ( BuiltinToolProjectionMap, CompositeProjectionMap, FileSystemProjectionMap, + ProjectionAwareToolClassifier, ProjectionMap, WebToolProjectionMap, compose_projection_maps, @@ -68,6 +89,7 @@ truncate_lines, truncate_text, ) +from .prompt_capabilities import AdapterPromptCapabilities from .providers import ( ApprovalStateProvider, ConfigOption, @@ -84,6 +106,14 @@ from .runtime.server import create_acp_agent, run_acp from .session.state import AcpSessionContext, JsonValue from .session.store import FileSessionStore, MemorySessionStore, SessionStore +from .slash import ( + SlashCommandHandler, + SlashCommandProvider, + SlashCommandRequest, + SlashCommandResult, + StaticSlashCommand, + StaticSlashCommandProvider, +) from .testing import BlackBoxHarness, RecordingACPClient, UpdateRecord, agent_message_texts from .types import ( AcpAgent, @@ -130,7 +160,10 @@ "caution_for_command", "caution_for_path", "DEFAULT_TEXT_TRUNCATION_MARKER", + "DefaultPermissionToolCallBuilder", "EmbeddedResourceContentBlock", + "EventEmissionMode", + "ExternalHookEventBridge", "FileSessionStore", "FileSystemProjectionMap", "FactoryAgentSource", @@ -166,16 +199,26 @@ "NativeApprovalBridge", "NativePlanPersistenceProvider", "OpenAICompactionBridge", + "AdapterPromptCapabilities", "PlanEntry", "PlanGenerationType", "PlanProvider", "PrefixToolsBridge", + "ApprovalPolicy", + "ApprovalPolicyStore", "ProjectionMap", + "ProjectionAwareApprovalBridge", + "ProjectionAwareToolClassifier", "PromptModelOverrideProvider", + "PermissionOptionSet", + "PermissionRequestContext", + "PermissionToolCallBuilder", "ResourceContentBlock", "format_code_block", "format_diff_preview", "format_terminal_status", + "PrepareOutputToolsBridge", + "PrepareOutputToolsMode", "PrepareToolsBridge", "PrepareToolsMode", "RegisteredHookInfo", @@ -189,9 +232,16 @@ "WebSearchBridge", "WebToolProjectionMap", "SessionStore", + "SessionMetadataApprovalPolicyStore", "SessionModelsProvider", "SessionModesProvider", + "SlashCommandHandler", + "SlashCommandProvider", + "SlashCommandRequest", + "SlashCommandResult", "StaticAgentSource", + "StaticSlashCommand", + "StaticSlashCommandProvider", "SseMcpServer", "single_line_summary", "TerminalBackend", @@ -204,4 +254,5 @@ "agent_message_texts", "create_acp_agent", "run_acp", + "supports_projection_aware_approval_bridge", ) diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/_version.py b/packages/adapters/pydantic-acp/src/pydantic_acp/_version.py index bd0f56b..e39e524 100644 --- a/packages/adapters/pydantic-acp/src/pydantic_acp/_version.py +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/_version.py @@ -2,4 +2,4 @@ __all__ = ("__version__",) -__version__ = "0.9.0" +__version__ = "0.9.6" diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/agent_source.py b/packages/adapters/pydantic-acp/src/pydantic_acp/agent_source.py index ed0813f..283aaa7 100644 --- a/packages/adapters/pydantic-acp/src/pydantic_acp/agent_source.py +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/agent_source.py @@ -6,7 +6,7 @@ from pydantic_ai import Agent as PydanticAgent -from .awaitables import is_awaitable, is_resolved +from .awaitables import resolve_value from .session.state import AcpSessionContext AgentFactoryDepsT = TypeVar("AgentFactoryDepsT", contravariant=True) @@ -61,11 +61,7 @@ class FactoryAgentSource(Generic[AgentDepsT, OutputDataT]): factory: AgentFactory[AgentDepsT, OutputDataT] async def get_agent(self, session: AcpSessionContext) -> PydanticAgent[AgentDepsT, OutputDataT]: - candidate = self.factory(session) - if is_awaitable(candidate): - return await candidate - assert is_resolved(candidate) - return candidate + return await resolve_value(self.factory(session)) async def get_deps( self, diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/approval_store.py b/packages/adapters/pydantic-acp/src/pydantic_acp/approval_store.py new file mode 100644 index 0000000..2ddbcbd --- /dev/null +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/approval_store.py @@ -0,0 +1,89 @@ +from __future__ import annotations as _annotations + +from dataclasses import dataclass +from typing import Literal, Protocol, TypeAlias + +from typing_extensions import TypeIs + +from .session.state import AcpSessionContext, JsonValue + +__all__ = ( + "ApprovalPolicy", + "ApprovalPolicyStore", + "PermissionOptionSet", + "SessionMetadataApprovalPolicyStore", +) + +ApprovalPolicy: TypeAlias = Literal["allow", "reject"] + + +def _is_approval_policy(value: JsonValue) -> TypeIs[ApprovalPolicy]: + return value in {"allow", "reject"} + + +class ApprovalPolicyStore(Protocol): + def get_policy( + self, + session: AcpSessionContext, + policy_key: str, + ) -> ApprovalPolicy | None: ... + + def set_policy( + self, + session: AcpSessionContext, + policy_key: str, + policy: ApprovalPolicy, + ) -> None: ... + + def export_state( + self, + session: AcpSessionContext, + ) -> dict[str, JsonValue] | None: ... + + +@dataclass(slots=True) +class SessionMetadataApprovalPolicyStore: + metadata_key: str = "approval_policies" + + def get_policy( + self, + session: AcpSessionContext, + policy_key: str, + ) -> ApprovalPolicy | None: + policy = self._policies(session).get(policy_key) + if _is_approval_policy(policy): + return policy + return None + + def set_policy( + self, + session: AcpSessionContext, + policy_key: str, + policy: ApprovalPolicy, + ) -> None: + raw_policies = session.metadata.get(self.metadata_key) + if not isinstance(raw_policies, dict): + raw_policies = {} + session.metadata[self.metadata_key] = raw_policies + raw_policies[policy_key] = policy + + def export_state( + self, + session: AcpSessionContext, + ) -> dict[str, JsonValue] | None: + policies = self._policies(session) + return dict(policies) if policies else None + + def _policies(self, session: AcpSessionContext) -> dict[str, JsonValue]: + raw_policies = session.metadata.get(self.metadata_key) + if isinstance(raw_policies, dict): + return raw_policies + return {} + + +@dataclass(frozen=True, slots=True, kw_only=True) +class PermissionOptionSet: + allow_once_name: str = "Allow" + reject_once_name: str = "Deny" + allow_always_name: str = "Always Allow" + reject_always_name: str = "Always Deny" diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/approvals.py b/packages/adapters/pydantic-acp/src/pydantic_acp/approvals.py index b18deb6..eb3a1d3 100644 --- a/packages/adapters/pydantic-acp/src/pydantic_acp/approvals.py +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/approvals.py @@ -1,27 +1,41 @@ from __future__ import annotations as _annotations -from dataclasses import dataclass -from typing import Final, Literal, Protocol, TypeAlias +from dataclasses import dataclass, field +from inspect import Parameter, signature +from typing import Protocol from acp.exceptions import RequestError from acp.interfaces import Client as AcpClient -from acp.schema import PermissionOption, ToolCallUpdate +from acp.schema import PermissionOption from pydantic_ai.messages import ToolCallPart from pydantic_ai.tools import DeferredToolRequests, DeferredToolResults, ToolApproved, ToolDenied from typing_extensions import TypeIs -from .projection import ToolClassifier, extract_tool_call_locations +from .approval_store import ( + ApprovalPolicy, + ApprovalPolicyStore, + PermissionOptionSet, + SessionMetadataApprovalPolicyStore, +) +from .permission_presentation import ( + DefaultPermissionToolCallBuilder, + PermissionRequestContext, + PermissionToolCallBuilder, +) +from .projection import ProjectionMap, ToolClassifier from .session.state import AcpSessionContext, JsonValue -ApprovalPolicy: TypeAlias = Literal["allow", "reject"] - -__all__ = ("ApprovalBridge", "ApprovalResolution", "NativeApprovalBridge") - -_APPROVAL_POLICIES_KEY: Final = "approval_policies" - - -def _is_approval_policy(value: JsonValue) -> TypeIs[ApprovalPolicy]: - return value in {"allow", "reject"} +__all__ = ( + "ApprovalBridge", + "ApprovalPolicy", + "ApprovalPolicyStore", + "ApprovalResolution", + "NativeApprovalBridge", + "PermissionOptionSet", + "ProjectionAwareApprovalBridge", + "SessionMetadataApprovalPolicyStore", + "supports_projection_aware_approval_bridge", +) @dataclass(slots=True, frozen=True, kw_only=True) @@ -42,9 +56,42 @@ async def resolve_deferred_approvals( ) -> ApprovalResolution: ... +class ProjectionAwareApprovalBridge(Protocol): + async def resolve_deferred_approvals( + self, + *, + client: AcpClient, + session: AcpSessionContext, + requests: DeferredToolRequests, + classifier: ToolClassifier, + projection_map: ProjectionMap | None = None, + ) -> ApprovalResolution: ... + + +def supports_projection_aware_approval_bridge( + value: object, +) -> TypeIs[ProjectionAwareApprovalBridge]: + resolver = getattr(value, "resolve_deferred_approvals", None) + if not callable(resolver): + return False + try: + resolver_signature = signature(resolver) + except (TypeError, ValueError): + return False + parameters = resolver_signature.parameters + if "projection_map" in parameters: + return True + return any(parameter.kind is Parameter.VAR_KEYWORD for parameter in parameters.values()) + + @dataclass(slots=True, kw_only=True) class NativeApprovalBridge: enable_persistent_choices: bool = False + tool_call_builder: PermissionToolCallBuilder = field( + default_factory=DefaultPermissionToolCallBuilder + ) + policy_store: ApprovalPolicyStore = field(default_factory=SessionMetadataApprovalPolicyStore) + option_set: PermissionOptionSet = field(default_factory=PermissionOptionSet) async def resolve_deferred_approvals( self, @@ -53,6 +100,7 @@ async def resolve_deferred_approvals( session: AcpSessionContext, requests: DeferredToolRequests, classifier: ToolClassifier, + projection_map: ProjectionMap | None = None, ) -> ApprovalResolution: deferred_results = DeferredToolResults(metadata=dict(requests.metadata)) for tool_call in requests.approvals: @@ -68,7 +116,16 @@ async def resolve_deferred_approvals( permission_response = await client.request_permission( options=self._build_permission_options(), session_id=session.session_id, - tool_call=self._build_tool_call_update(tool_call, classifier), + tool_call=self.tool_call_builder.build_tool_call_update( + PermissionRequestContext( + session=session, + tool_call=tool_call, + raw_input=dict(raw_input), + cwd=session.cwd, + classifier=classifier, + projection_map=projection_map, + ) + ), ) outcome = permission_response.outcome if outcome.outcome == "cancelled": @@ -92,31 +149,34 @@ async def resolve_deferred_approvals( def _build_permission_options(self) -> list[PermissionOption]: options = [ - PermissionOption(option_id="allow_once", name="Allow", kind="allow_once"), - PermissionOption(option_id="reject_once", name="Deny", kind="reject_once"), + PermissionOption( + option_id="allow_once", + name=self.option_set.allow_once_name, + kind="allow_once", + ), + PermissionOption( + option_id="reject_once", + name=self.option_set.reject_once_name, + kind="reject_once", + ), ] if not self.enable_persistent_choices: return options return [ - PermissionOption(option_id="allow_once", name="Allow Once", kind="allow_once"), - PermissionOption(option_id="allow_always", name="Always Allow", kind="allow_always"), - PermissionOption(option_id="reject_once", name="Deny Once", kind="reject_once"), - PermissionOption(option_id="reject_always", name="Always Deny", kind="reject_always"), + options[0], + PermissionOption( + option_id="allow_always", + name=self.option_set.allow_always_name, + kind="allow_always", + ), + options[1], + PermissionOption( + option_id="reject_always", + name=self.option_set.reject_always_name, + kind="reject_always", + ), ] - def _build_tool_call_update( - self, tool_call: ToolCallPart, classifier: ToolClassifier - ) -> ToolCallUpdate: - raw_input = tool_call.args_as_dict() - return ToolCallUpdate( - tool_call_id=tool_call.tool_call_id, - title=tool_call.tool_name, - kind=classifier.classify(tool_call.tool_name, raw_input), - locations=extract_tool_call_locations(raw_input), - raw_input=raw_input, - status="in_progress", - ) - def _selected_option_to_result( self, option_id: str, @@ -154,11 +214,9 @@ def _get_remembered_policy( session: AcpSessionContext, approval_policy_key: str, ) -> ApprovalPolicy | None: - policies = self._approval_policies(session) - remembered = policies.get(approval_policy_key) - if _is_approval_policy(remembered): - return remembered - return None + if not self.enable_persistent_choices: + return None + return self.policy_store.get_policy(session, approval_policy_key) def _set_remembered_policy( self, @@ -167,17 +225,11 @@ def _set_remembered_policy( approval_policy_key: str, policy: ApprovalPolicy, ) -> None: - raw_policies = session.metadata.get(_APPROVAL_POLICIES_KEY) - if not isinstance(raw_policies, dict): - raw_policies = {} - session.metadata[_APPROVAL_POLICIES_KEY] = raw_policies - raw_policies[approval_policy_key] = policy + self.policy_store.set_policy(session, approval_policy_key, policy) def _approval_policies(self, session: AcpSessionContext) -> dict[str, JsonValue]: - raw_policies = session.metadata.get(_APPROVAL_POLICIES_KEY) - if isinstance(raw_policies, dict): - return raw_policies - return {} + exported = self.policy_store.export_state(session) + return exported if exported is not None else {} def _policy_to_result(self, policy: ApprovalPolicy) -> ToolApproved | ToolDenied: if policy == "allow": diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/awaitables.py b/packages/adapters/pydantic-acp/src/pydantic_acp/awaitables.py index 5b8d633..e05c8b7 100644 --- a/packages/adapters/pydantic-acp/src/pydantic_acp/awaitables.py +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/awaitables.py @@ -2,7 +2,7 @@ import inspect from collections.abc import Awaitable -from typing import TypeVar +from typing import TypeVar, cast from typing_extensions import TypeIs @@ -21,6 +21,6 @@ def is_resolved(value: ValueT | Awaitable[ValueT]) -> TypeIs[ValueT]: async def resolve_value(value: ValueT | Awaitable[ValueT]) -> ValueT: if is_awaitable(value): - return await value + return await cast(Awaitable[ValueT], value) assert is_resolved(value) return value diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/__init__.py b/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/__init__.py index bf8e2b4..5a60350 100644 --- a/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/__init__.py +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/__init__.py @@ -14,6 +14,7 @@ WebFetchBridge, WebSearchBridge, ) +from .external_hooks import EventEmissionMode, ExternalHookEventBridge from .history_processor import ( HistoryProcessorBridge, HistoryProcessorCallable, @@ -24,13 +25,21 @@ ) from .hooks import HookBridge from .mcp import McpBridge, McpServerDefinition, McpToolDefinition -from .prepare_tools import PlanGenerationType, PrepareToolsBridge, PrepareToolsMode +from .prepare_tools import ( + PlanGenerationType, + PrepareOutputToolsBridge, + PrepareOutputToolsMode, + PrepareToolsBridge, + PrepareToolsMode, +) from .thinking import ThinkingBridge __all__ = ( "BufferedCapabilityBridge", "CapabilityBridge", "AnthropicCompactionBridge", + "EventEmissionMode", + "ExternalHookEventBridge", "HistoryProcessorCallable", "HistoryProcessorBridge", "HistoryProcessorContextual", @@ -47,6 +56,8 @@ "OpenAICompactionBridge", "PlanGenerationType", "PrefixToolsBridge", + "PrepareOutputToolsBridge", + "PrepareOutputToolsMode", "PrepareToolsBridge", "PrepareToolsMode", "SetToolMetadataBridge", diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/_hook_capability.py b/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/_hook_capability.py index 554d41c..aa1e202 100644 --- a/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/_hook_capability.py +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/_hook_capability.py @@ -17,7 +17,7 @@ WrapToolValidateHandler, ) from pydantic_ai.messages import AgentStreamEvent, ModelResponse, ToolCallPart -from pydantic_ai.tools import RunContext, ToolDefinition +from pydantic_ai.tools import DeferredToolRequests, DeferredToolResults, RunContext, ToolDefinition from ..session.state import AcpSessionContext @@ -32,8 +32,12 @@ def build_hook_capability(bridge: HookBridge, session: AcpSessionContext) -> Hoo hook_kwargs.update(_event_stream_hook_kwargs(bridge, session)) hook_kwargs.update(_model_request_hook_kwargs(bridge, session)) hook_kwargs.update(_prepare_tools_hook_kwargs(bridge, session)) + hook_kwargs.update(_prepare_output_tools_hook_kwargs(bridge, session)) hook_kwargs.update(_tool_validation_hook_kwargs(bridge, session)) hook_kwargs.update(_tool_execution_hook_kwargs(bridge, session)) + hook_kwargs.update(_output_validation_hook_kwargs(bridge, session)) + hook_kwargs.update(_output_processing_hook_kwargs(bridge, session)) + hook_kwargs.update(_deferred_tool_calls_hook_kwargs(bridge, session)) return Hooks(**hook_kwargs) @@ -49,6 +53,8 @@ def enabled_hook_events(bridge: HookBridge) -> list[str]: enabled.extend(["before_model_request", "wrap_model_request", "after_model_request"]) if bridge._prepare_tools_enabled: enabled.append("prepare_tools") + if bridge._prepare_output_tools_enabled: + enabled.append("prepare_output_tools") if bridge._tool_validation_enabled: enabled.extend(["before_tool_validate", "wrap_tool_validate", "after_tool_validate"]) if bridge._tool_execution_enabled: @@ -60,6 +66,26 @@ def enabled_hook_events(bridge: HookBridge) -> list[str]: "on_tool_execute_error", ] ) + if bridge._output_validation_enabled: + enabled.extend( + [ + "before_output_validate", + "wrap_output_validate", + "after_output_validate", + "on_output_validate_error", + ] + ) + if bridge._output_processing_enabled: + enabled.extend( + [ + "before_output_process", + "wrap_output_process", + "after_output_process", + "on_output_process_error", + ] + ) + if bridge._deferred_tool_calls_enabled: + enabled.append("handle_deferred_tool_calls") return enabled @@ -329,6 +355,29 @@ async def prepare_tools( return {"prepare_tools": prepare_tools} +def _prepare_output_tools_hook_kwargs( + bridge: HookBridge, + session: AcpSessionContext, +) -> dict[str, Any]: + if not bridge._prepare_output_tools_enabled: + return {} + + async def prepare_output_tools( + ctx: RunContext[Any], + tool_defs: list[ToolDefinition], + ) -> list[ToolDefinition]: + del ctx + if bridge._prepare_output_tools_enabled: + bridge._record_completed_event( + session, + title="hook.prepare_output_tools", + raw_output=f"tools={len(tool_defs)}", + ) + return tool_defs + + return {"prepare_output_tools": prepare_output_tools} + + def _tool_validation_hook_kwargs(bridge: HookBridge, session: AcpSessionContext) -> dict[str, Any]: if not bridge._tool_validation_enabled: return {} @@ -495,3 +544,201 @@ async def on_tool_execute_error( "tool_execute": wrap_tool_execute, "tool_execute_error": on_tool_execute_error, } + + +def _output_validation_hook_kwargs( + bridge: HookBridge, + session: AcpSessionContext, +) -> dict[str, Any]: + if not bridge._output_validation_enabled: + return {} + + async def before_output_validate( + ctx: RunContext[Any], + *, + output_context: Any, + output: Any, + ) -> Any: + del ctx, output_context + if bridge._output_validation_enabled: + bridge._record_completed_event( + session, + title="hook.before_output_validate", + raw_output=str(output), + ) + return output + + async def wrap_output_validate( + ctx: RunContext[Any], + *, + output_context: Any, + output: Any, + handler: Any, + ) -> Any: + del ctx, output_context + try: + result = await handler(output) + except Exception as error: + if bridge._output_validation_enabled: + bridge._record_failed_event( + session, + title="hook.wrap_output_validate", + raw_output=str(error), + ) + raise + if bridge._output_validation_enabled: + bridge._record_completed_event( + session, + title="hook.wrap_output_validate", + raw_output=str(result), + ) + return result + + async def after_output_validate( + ctx: RunContext[Any], + *, + output_context: Any, + output: Any, + ) -> Any: + del ctx, output_context + if bridge._output_validation_enabled: + bridge._record_completed_event( + session, + title="hook.after_output_validate", + raw_output=str(output), + ) + return output + + async def on_output_validate_error( + ctx: RunContext[Any], + *, + output_context: Any, + output: Any, + error: Exception, + ) -> Any: + del ctx, output_context + if bridge._output_validation_enabled: + bridge._record_failed_event( + session, + title="hook.on_output_validate_error", + raw_output=str(error), + ) + raise error + + return { + "after_output_validate": after_output_validate, + "before_output_validate": before_output_validate, + "output_validate": wrap_output_validate, + "output_validate_error": on_output_validate_error, + } + + +def _output_processing_hook_kwargs( + bridge: HookBridge, + session: AcpSessionContext, +) -> dict[str, Any]: + if not bridge._output_processing_enabled: + return {} + + async def before_output_process( + ctx: RunContext[Any], + *, + output_context: Any, + output: Any, + ) -> Any: + del ctx, output_context + if bridge._output_processing_enabled: + bridge._record_completed_event( + session, + title="hook.before_output_process", + raw_output=str(output), + ) + return output + + async def wrap_output_process( + ctx: RunContext[Any], + *, + output_context: Any, + output: Any, + handler: Any, + ) -> Any: + del ctx, output_context + try: + result = await handler(output) + except Exception as error: + if bridge._output_processing_enabled: + bridge._record_failed_event( + session, + title="hook.wrap_output_process", + raw_output=str(error), + ) + raise + if bridge._output_processing_enabled: + bridge._record_completed_event( + session, + title="hook.wrap_output_process", + raw_output=str(result), + ) + return result + + async def after_output_process( + ctx: RunContext[Any], + *, + output_context: Any, + output: Any, + ) -> Any: + del ctx, output_context + if bridge._output_processing_enabled: + bridge._record_completed_event( + session, + title="hook.after_output_process", + raw_output=str(output), + ) + return output + + async def on_output_process_error( + ctx: RunContext[Any], + *, + output_context: Any, + output: Any, + error: Exception, + ) -> Any: + del ctx, output_context + if bridge._output_processing_enabled: + bridge._record_failed_event( + session, + title="hook.on_output_process_error", + raw_output=str(error), + ) + raise error + + return { + "after_output_process": after_output_process, + "before_output_process": before_output_process, + "output_process": wrap_output_process, + "output_process_error": on_output_process_error, + } + + +def _deferred_tool_calls_hook_kwargs( + bridge: HookBridge, + session: AcpSessionContext, +) -> dict[str, Any]: + if not bridge._deferred_tool_calls_enabled: + return {} + + async def deferred_tool_calls( + ctx: RunContext[Any], + *, + requests: DeferredToolRequests, + ) -> DeferredToolResults | None: + del ctx, requests + if bridge._deferred_tool_calls_enabled: + bridge._record_completed_event( + session, + title="hook.handle_deferred_tool_calls", + raw_output="deferred tool requests observed", + ) + return None + + return {"deferred_tool_calls": deferred_tool_calls} diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/capability_support.py b/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/capability_support.py index 938a462..3f3ac7a 100644 --- a/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/capability_support.py +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/capability_support.py @@ -408,7 +408,6 @@ def __init__(self) -> None: super().__init__( message_count_threshold=bridge.message_count_threshold, trigger=bridge.trigger, - instructions=bridge.instructions, ) async def before_model_request( @@ -428,7 +427,7 @@ async def before_model_request( title="Context Compaction", raw_input={ "provider": "openai", - "instructions": _json_string(self.instructions), + "instructions": _json_string(bridge.instructions), "message_count": len(request_context.messages), }, ) diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/external_hooks.py b/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/external_hooks.py new file mode 100644 index 0000000..ed7e9d8 --- /dev/null +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/external_hooks.py @@ -0,0 +1,70 @@ +from __future__ import annotations as _annotations + +from dataclasses import dataclass, field +from typing import Literal, TypeAlias + +from ..agent_types import RuntimeAgent +from ..hook_projection import HookEvent, HookProjectionMap +from ..session.state import AcpSessionContext, JsonValue +from .base import BufferedCapabilityBridge + +__all__ = ("EventEmissionMode", "ExternalHookEventBridge") + +EventEmissionMode: TypeAlias = Literal["paired", "start_only"] + + +@dataclass(slots=True, kw_only=True) +class ExternalHookEventBridge(BufferedCapabilityBridge): + metadata_key: str | None = "external_hooks" + projection_map: HookProjectionMap = field(default_factory=HookProjectionMap) + emission_mode: EventEmissionMode = "paired" + + def record_event( + self, + session: AcpSessionContext, + event: HookEvent, + *, + emission_mode: EventEmissionMode | None = None, + ) -> None: + mode = self.emission_mode if emission_mode is None else emission_mode + tool_call_id = self._next_event_id(session) + start_update = self.projection_map.build_start_update( + tool_call_id=tool_call_id, + event=event, + ) + if start_update is None: + return + if mode == "start_only": + if event.status is not None: + start_update.status = event.status + self._append_updates(session, [start_update]) + return + progress_update = self.projection_map.build_progress_update( + tool_call_id=tool_call_id, + event=event, + ) + if progress_update is None: + self._append_updates(session, [start_update]) + return + self._append_updates(session, [start_update, progress_update]) + + def get_session_metadata( + self, + session: AcpSessionContext, + agent: RuntimeAgent, + ) -> dict[str, JsonValue]: + del agent + hidden_event_ids: list[JsonValue] = [] + hidden_event_ids.extend(sorted(self.projection_map.hidden_event_ids)) + metadata: dict[str, JsonValue] = { + "emission_mode": self.emission_mode, + "pending_event_count": len(self._pending_updates.get(session.session_id, ())), + "hidden_event_ids": hidden_event_ids, + "projection_title_prefix": self.projection_map.title_prefix, + } + return metadata + + def _next_event_id(self, session: AcpSessionContext) -> str: + next_count = self._event_counts.get(session.session_id, 0) + 1 + self._event_counts[session.session_id] = next_count + return f"{session.session_id}:external-hook:{next_count}" diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/hooks.py b/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/hooks.py index d6aa7d7..888cd4b 100644 --- a/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/hooks.py +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/hooks.py @@ -20,6 +20,10 @@ class HookBridge(BufferedCapabilityBridge): record_event_stream: bool = True record_model_requests: bool = True record_node_lifecycle: bool = True + record_deferred_tool_calls: bool = True + record_output_processing: bool = True + record_output_validation: bool = True + record_prepare_output_tools: bool = True record_prepare_tools: bool = True record_run_lifecycle: bool = True record_tool_execution: bool = True @@ -55,6 +59,22 @@ def _model_requests_enabled(self) -> bool: def _node_lifecycle_enabled(self) -> bool: return not self.hide_all and self.record_node_lifecycle + @property + def _deferred_tool_calls_enabled(self) -> bool: + return not self.hide_all and self.record_deferred_tool_calls + + @property + def _output_processing_enabled(self) -> bool: + return not self.hide_all and self.record_output_processing + + @property + def _output_validation_enabled(self) -> bool: + return not self.hide_all and self.record_output_validation + + @property + def _prepare_output_tools_enabled(self) -> bool: + return not self.hide_all and self.record_prepare_output_tools + @property def _prepare_tools_enabled(self) -> bool: return not self.hide_all and self.record_prepare_tools diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/prepare_tools.py b/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/prepare_tools.py index b641357..ff43584 100644 --- a/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/prepare_tools.py +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/bridges/prepare_tools.py @@ -4,7 +4,7 @@ from typing import Final, Generic, Literal, TypeVar, cast from acp.schema import SessionConfigOptionSelect, SessionConfigSelectOption, SessionMode -from pydantic_ai.capabilities import PrepareTools +from pydantic_ai.capabilities import PrepareOutputTools, PrepareTools from pydantic_ai.tools import RunContext, ToolDefinition, ToolsPrepareFunc from .._slash_commands import validate_mode_command_ids @@ -24,6 +24,8 @@ __all__ = ( "PlanGenerationType", + "PrepareOutputToolsBridge", + "PrepareOutputToolsMode", "PrepareToolsBridge", "PrepareToolsMode", ) @@ -39,6 +41,14 @@ class PrepareToolsMode(Generic[AgentDepsT]): plan_tools: bool = False +@dataclass(slots=True, frozen=True, kw_only=True) +class PrepareOutputToolsMode(Generic[AgentDepsT]): + id: str + name: str + prepare_func: ToolsPrepareFunc[AgentDepsT] + description: str | None = None + + @dataclass(slots=True, kw_only=True) class PrepareToolsBridge(BufferedCapabilityBridge, Generic[AgentDepsT]): metadata_key: str | None = "prepare_tools" @@ -160,7 +170,7 @@ def get_config_options( options=[ SessionConfigSelectOption( value=value, - name="Tool-Based" if value == "tools" else "Structured", + name="Tool Plans" if value == "tools" else "Structured Plans", ) for value in _PLAN_GENERATION_CONFIG_OPTIONS ], @@ -261,3 +271,135 @@ def supports_plan_write_tools(self, session: AcpSessionContext) -> bool: def supports_plan_progress(self, session: AcpSessionContext) -> bool: return self.current_mode(session).plan_tools + + +@dataclass(slots=True, kw_only=True) +class PrepareOutputToolsBridge(BufferedCapabilityBridge, Generic[AgentDepsT]): + metadata_key: str | None = "prepare_output_tools" + default_mode_id: str + modes: list[PrepareOutputToolsMode[AgentDepsT]] + mode_config_key: str = "prepare_output_tools_mode" + + def __post_init__(self) -> None: + if not self.modes: + raise ValueError("PrepareOutputToolsBridge requires at least one mode.") + validate_mode_command_ids(mode.id for mode in self.modes) + mode_ids = {mode.id for mode in self.modes} + if self.default_mode_id not in mode_ids: + raise ValueError("PrepareOutputToolsBridge default mode must match one of the modes.") + + def build_prepare_output_tools( + self, + session: AcpSessionContext, + ) -> ToolsPrepareFunc[AgentDepsT]: + async def prepare_output_tools( + ctx: RunContext[AgentDepsT], + tool_defs: list[ToolDefinition], + ) -> list[ToolDefinition]: + mode = self._require_mode(self._current_mode_id(session)) + try: + prepared = mode.prepare_func(ctx, list(tool_defs)) + resolved = await resolve_value(prepared) + except Exception as error: + self._record_failed_event( + session, + title=f"prepare_output_tools.{mode.id}", + raw_output=str(error), + ) + raise + + next_tool_defs = list(tool_defs if resolved is None else resolved) + self._record_completed_event( + session, + title=f"prepare_output_tools.{mode.id}", + raw_output=f"tools={len(next_tool_defs)}/{len(tool_defs)}", + ) + return next_tool_defs + + return prepare_output_tools + + def build_capability(self, session: AcpSessionContext) -> PrepareOutputTools[AgentDepsT]: + return PrepareOutputTools(self.build_prepare_output_tools(session)) + + def build_agent_capabilities( + self, + session: AcpSessionContext, + ) -> tuple[PrepareOutputTools[AgentDepsT], ...]: + return (self.build_capability(session),) + + def get_mode_state( + self, + session: AcpSessionContext, + agent: RuntimeAgent, + ) -> ModeState: + del agent + return ModeState( + modes=[ + SessionMode( + id=mode.id, + name=mode.name, + description=mode.description, + ) + for mode in self.modes + ], + current_mode_id=self._current_mode_id(session), + ) + + def get_session_metadata( + self, + session: AcpSessionContext, + agent: RuntimeAgent, + ) -> dict[str, JsonValue]: + del agent + modes: list[JsonValue] = [ + { + "description": mode.description, + "id": mode.id, + "name": mode.name, + } + for mode in self.modes + ] + return { + "current_mode_id": self._current_mode_id(session), + "modes": modes, + } + + def set_mode( + self, + session: AcpSessionContext, + agent: RuntimeAgent, + mode_id: str, + ) -> ModeState | None: + del agent + if self._find_mode(mode_id) is None: + return None + session.config_values[self.mode_config_key] = mode_id + return ModeState( + modes=[ + SessionMode( + id=mode.id, + name=mode.name, + description=mode.description, + ) + for mode in self.modes + ], + current_mode_id=mode_id, + ) + + def _current_mode_id(self, session: AcpSessionContext) -> str: + configured_mode = session.config_values.get(self.mode_config_key) + if isinstance(configured_mode, str) and self._find_mode(configured_mode) is not None: + return configured_mode + return self.default_mode_id + + def _find_mode(self, mode_id: str) -> PrepareOutputToolsMode[AgentDepsT] | None: + for mode in self.modes: + if mode.id == mode_id: + return mode + return None + + def _require_mode(self, mode_id: str) -> PrepareOutputToolsMode[AgentDepsT]: + mode = self._find_mode(mode_id) + if mode is None: + raise ValueError(f"Unknown prepare output tools mode: {mode_id!r}") + return mode diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/config.py b/packages/adapters/pydantic-acp/src/pydantic_acp/config.py index 14c815a..4008b26 100644 --- a/packages/adapters/pydantic-acp/src/pydantic_acp/config.py +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/config.py @@ -10,6 +10,7 @@ from .host import HostAccessPolicy from .models import AdapterModel from .projection import DefaultToolClassifier, ProjectionMap, ToolClassifier +from .prompt_capabilities import AdapterPromptCapabilities from .providers import ( ApprovalStateProvider, ConfigOptionsProvider, @@ -21,6 +22,7 @@ ) from .serialization import DefaultOutputSerializer, OutputSerializer from .session.store import MemorySessionStore, SessionStore +from .slash import SlashCommandProvider DEFAULT_AGENT_NAME = "pydantic-acp" DEFAULT_AGENT_TITLE = "Pydantic ACP" @@ -53,8 +55,12 @@ class AdapterConfig: native_plan_additional_instructions: str | None = None native_plan_persistence_provider: NativePlanPersistenceProvider | None = None plan_provider: PlanProvider | None = None + prompt_capabilities: AdapterPromptCapabilities = field( + default_factory=AdapterPromptCapabilities + ) prompt_model_override_provider: PromptModelOverrideProvider | None = None replay_history_on_load: bool = True + slash_command_provider: SlashCommandProvider | None = None available_models: list[AdapterModel] = field(default_factory=list) session_store: SessionStore = field(default_factory=MemorySessionStore) output_serializer: OutputSerializer = field(default_factory=DefaultOutputSerializer) diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/hook_projection.py b/packages/adapters/pydantic-acp/src/pydantic_acp/hook_projection.py index 2629cac..4c243e5 100644 --- a/packages/adapters/pydantic-acp/src/pydantic_acp/hook_projection.py +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/hook_projection.py @@ -19,14 +19,23 @@ def _default_event_labels() -> dict[str, str]: return { "before_model_request": "Before Model", "before_node_run": "Before Node", + "before_output_process": "Before Output", + "before_output_validate": "Before Output Validate", "before_run": "Before Run", "before_tool_execute": "Before Tool", "before_tool_validate": "Before Validate", + "deferred_tool_calls": "Deferred Tools", "event": "Event", "model_request": "Model Request", "model_request_error": "Model Request Error", "node_run": "Node Run", "node_run_error": "Node Run Error", + "output_process": "Output Process", + "output_process_error": "Output Process Error", + "output_validate": "Output Validate", + "output_validate_error": "Output Validate Error", + "prepare_output_tools": "Prepare Output Tools", + "prepare_tools": "Prepare Tools", "run": "Run", "run_error": "Run Error", "run_event_stream": "Run Stream", @@ -41,14 +50,23 @@ def _default_event_kinds() -> dict[str, ToolKind]: return { "before_model_request": "fetch", "before_node_run": "execute", + "before_output_process": "think", + "before_output_validate": "think", "before_run": "think", "before_tool_execute": "execute", "before_tool_validate": "execute", + "deferred_tool_calls": "execute", "event": "execute", "model_request": "fetch", "model_request_error": "fetch", "node_run": "execute", "node_run_error": "execute", + "output_process": "think", + "output_process_error": "think", + "output_validate": "think", + "output_validate_error": "think", + "prepare_output_tools": "think", + "prepare_tools": "think", "run": "think", "run_error": "think", "run_event_stream": "fetch", diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/permission_presentation.py b/packages/adapters/pydantic-acp/src/pydantic_acp/permission_presentation.py new file mode 100644 index 0000000..edace97 --- /dev/null +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/permission_presentation.py @@ -0,0 +1,68 @@ +from __future__ import annotations as _annotations + +from dataclasses import dataclass +from pathlib import Path +from typing import Protocol + +from acp.schema import ToolCallStatus, ToolCallUpdate +from pydantic_ai.messages import ToolCallPart + +from .projection import ProjectionMap, ToolClassifier, extract_tool_call_locations +from .session.state import AcpSessionContext + +__all__ = ( + "DefaultPermissionToolCallBuilder", + "PermissionRequestContext", + "PermissionToolCallBuilder", +) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class PermissionRequestContext: + session: AcpSessionContext + tool_call: ToolCallPart + raw_input: dict[str, object] + cwd: Path + classifier: ToolClassifier + projection_map: ProjectionMap | None = None + + +class PermissionToolCallBuilder(Protocol): + def build_tool_call_update( + self, + context: PermissionRequestContext, + ) -> ToolCallUpdate: ... + + +@dataclass(frozen=True, slots=True, kw_only=True) +class DefaultPermissionToolCallBuilder: + status: ToolCallStatus = "pending" + + def build_tool_call_update( + self, + context: PermissionRequestContext, + ) -> ToolCallUpdate: + projection = None + if context.projection_map is not None: + projection = context.projection_map.project_start( + context.tool_call.tool_name, + cwd=context.cwd, + raw_input=context.raw_input, + ) + return ToolCallUpdate( + tool_call_id=context.tool_call.tool_call_id, + title=( + projection.title + if projection is not None and projection.title is not None + else context.tool_call.tool_name + ), + kind=context.classifier.classify(context.tool_call.tool_name, context.raw_input), + content=projection.content if projection is not None else None, + locations=( + projection.locations + if projection is not None and projection.locations is not None + else extract_tool_call_locations(context.raw_input) + ), + raw_input=context.raw_input, + status=self.status, + ) diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/projection.py b/packages/adapters/pydantic-acp/src/pydantic_acp/projection.py index 44708d4..22cf6e5 100644 --- a/packages/adapters/pydantic-acp/src/pydantic_acp/projection.py +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/projection.py @@ -39,6 +39,7 @@ "DefaultToolClassifier", "FileSystemProjectionMap", "ProjectionMap", + "ProjectionAwareToolClassifier", "ToolClassifier", "WebToolProjectionMap", "build_tool_progress_update", @@ -60,6 +61,7 @@ _CONTENT_KEYS = ("content", "text", "new_text") _OLD_TEXT_KEYS = ("old_text", "oldText", "previous_content", "previous_text") _COMMAND_KEYS = ("command", "cmd", "script", "bash") +_SEARCH_PATTERN_KEYS = ("pattern", "query", "q", "glob", "search") _TERMINAL_ID_KEYS = ("terminal_id", "terminalId") _MAX_COMMAND_PREVIEW_CHARS = 4000 _MAX_COMMAND_TITLE_CHARS = 80 @@ -169,14 +171,21 @@ class FileSystemProjectionMap: write_tool_names: frozenset[str] = frozenset() read_tool_names: frozenset[str] = frozenset() bash_tool_names: frozenset[str] = frozenset() + search_tool_names: frozenset[str] = frozenset() default_write_tool: str | None = None default_read_tool: str | None = None default_bash_tool: str | None = None + default_search_tool: str | None = None path_arg: str | None = None content_arg: str | None = None old_text_arg: str | None = None command_arg: str | None = None terminal_id_arg: str | None = None + search_path_arg: str | None = None + search_pattern_arg: str | None = None + render_search_results_as_tree: bool = False + hide_dot_directories_in_tree: bool = True + tree_root_label: str | None = None def project_start( self, @@ -185,6 +194,15 @@ def project_start( cwd: Path | None = None, raw_input: Any = None, ) -> ToolProjection | None: + if tool_name in self._search_tool_names(): + if not _is_string_keyed_object_dict(raw_input): + return None + search_text = self._format_search_start(raw_input) + return ToolProjection( + content=[ContentToolCallContent(type="content", content=_text_block(search_text))], + locations=self._search_locations_from_input(raw_input), + title=self._format_search_title(raw_input), + ) if tool_name in self._bash_tool_names(): if not _is_string_keyed_object_dict(raw_input): return None @@ -246,6 +264,32 @@ def project_progress( ), status=status_override, ) + if tool_name in self._search_tool_names(): + if not _is_string_keyed_object_dict(raw_input): + return None + output_text = _stringify_value(raw_output, serialized_output) + if ( + status == "completed" + and self.render_search_results_as_tree + and not _looks_like_status_or_error_output(output_text) + ): + rendered_output = _render_path_tree( + output_text, + root_label=self._tree_root_label(raw_input), + hide_dot_directories=self.hide_dot_directories_in_tree, + ) + else: + rendered_output = output_text + return ToolProjection( + content=[ + ContentToolCallContent( + type="content", + content=_text_block(rendered_output), + ) + ], + locations=self._search_locations_from_input(raw_input), + status=status, + ) if status != "completed": return None if tool_name in self._write_tool_names(): @@ -292,6 +336,12 @@ def _bash_tool_names(self) -> frozenset[str]: names.add(self.default_bash_tool) return frozenset(names) + def _search_tool_names(self) -> frozenset[str]: + names = set(self.search_tool_names) + if self.default_search_tool is not None: + names.add(self.default_search_tool) + return frozenset(names) + def _path_from_input(self, raw_input: dict[str, Any]) -> str | None: for key in _candidate_keys(self.path_arg, _PATH_KEYS): value = raw_input.get(key) @@ -313,6 +363,54 @@ def _command_from_input(self, raw_input: dict[str, Any]) -> str | None: return value return None + def _search_path_from_input(self, raw_input: dict[str, Any]) -> str | None: + for key in _candidate_keys(self.search_path_arg, _PATH_KEYS): + value = raw_input.get(key) + if isinstance(value, str) and value: + return value + return None + + def _search_pattern_from_input(self, raw_input: dict[str, Any]) -> str | None: + for key in _candidate_keys(self.search_pattern_arg, _SEARCH_PATTERN_KEYS): + value = raw_input.get(key) + if isinstance(value, str) and value: + return value + return None + + def _search_locations_from_input( + self, raw_input: dict[str, Any] + ) -> list[ToolCallLocation] | None: + path = self._search_path_from_input(raw_input) + return [ToolCallLocation(path=path)] if path is not None else None + + def _format_search_title(self, raw_input: dict[str, Any]) -> str: + pattern = self._search_pattern_from_input(raw_input) + path = self._search_path_from_input(raw_input) + if pattern is not None and path is not None: + return ( + f"Search {path} for {_single_line_preview(pattern, limit=_MAX_COMMAND_TITLE_CHARS)}" + ) + if pattern is not None: + return f"Search for {_single_line_preview(pattern, limit=_MAX_COMMAND_TITLE_CHARS)}" + if path is not None: + return f"List {path}" + return "Search files" + + def _format_search_start(self, raw_input: dict[str, Any]) -> str: + lines: list[str] = [] + path = self._search_path_from_input(raw_input) + pattern = self._search_pattern_from_input(raw_input) + if path is not None: + lines.append(f"Path: {path}") + if pattern is not None: + lines.append(f"Pattern: {pattern}") + return "\n".join(lines) if lines else "Searching files." + + def _tree_root_label(self, raw_input: dict[str, Any]) -> str: + if self.tree_root_label is not None: + return self.tree_root_label + return self._search_path_from_input(raw_input) or "." + def _old_text_from_input( self, raw_input: dict[str, Any], @@ -542,6 +640,57 @@ def approval_policy_key(self, tool_name: str, raw_input: Any = None) -> str: return tool_name +@dataclass(slots=True, frozen=True, kw_only=True) +class ProjectionAwareToolClassifier: + base_classifier: ToolClassifier + projection_maps: Sequence[ProjectionMap] + + def classify(self, tool_name: str, raw_input: Any = None) -> ToolKind: + for projection_map in self.projection_maps: + if isinstance(projection_map, FileSystemProjectionMap): + if tool_name in projection_map._read_tool_names(): + return "read" + if tool_name in projection_map._write_tool_names(): + return "edit" + if tool_name in projection_map._bash_tool_names(): + return "execute" + if tool_name in projection_map._search_tool_names(): + return "search" + if isinstance(projection_map, CompositeProjectionMap): + nested_classifier = ProjectionAwareToolClassifier( + base_classifier=self.base_classifier, + projection_maps=projection_map.maps, + ) + nested_kind = nested_classifier._projection_kind(tool_name) + if nested_kind is not None: + return nested_kind + return self.base_classifier.classify(tool_name, raw_input) + + def approval_policy_key(self, tool_name: str, raw_input: Any = None) -> str: + return self.base_classifier.approval_policy_key(tool_name, raw_input) + + def _projection_kind(self, tool_name: str) -> ToolKind | None: + for projection_map in self.projection_maps: + if isinstance(projection_map, FileSystemProjectionMap): + if tool_name in projection_map._read_tool_names(): + return "read" + if tool_name in projection_map._write_tool_names(): + return "edit" + if tool_name in projection_map._bash_tool_names(): + return "execute" + if tool_name in projection_map._search_tool_names(): + return "search" + if isinstance(projection_map, CompositeProjectionMap): + nested_classifier = ProjectionAwareToolClassifier( + base_classifier=self.base_classifier, + projection_maps=projection_map.maps, + ) + nested_kind = nested_classifier._projection_kind(tool_name) + if nested_kind is not None: + return nested_kind + return None + + def _is_output_tool(tool_name: str) -> bool: return tool_name == "final_result" @@ -557,6 +706,97 @@ def extract_tool_call_locations(raw_input: Any) -> list[ToolCallLocation] | None return None +@dataclass(slots=True) +class _PathTreeNode: + directories: dict[str, _PathTreeNode] = field(default_factory=dict) + files: set[str] = field(default_factory=set) + + +def _looks_like_status_or_error_output(text: str) -> bool: + stripped = text.strip().lower() + if not stripped: + return True + return stripped.startswith(("error:", "failed:", "status:", "traceback")) + + +def _render_path_tree( + text: str, + *, + root_label: str, + hide_dot_directories: bool, +) -> str: + root = _PathTreeNode() + has_path = False + truncated = False + for raw_line in text.splitlines(): + line = raw_line.strip() + if not line: + continue + if line == "...": + truncated = True + continue + normalized = line.removeprefix("./") + if not normalized: + continue + if _path_has_hidden_directory(normalized, hide_dot_directories=hide_dot_directories): + continue + _insert_tree_path(root, normalized) + has_path = True + if not has_path: + return text + lines = [f"Tree: {root_label}", ""] + entries = _render_tree_node(root, prefix="") + lines.extend(entries) + if truncated: + lines.append("...") + return "\n".join(lines).rstrip() + + +def _path_has_hidden_directory(path: str, *, hide_dot_directories: bool) -> bool: + if not hide_dot_directories: + return False + parts = [part for part in path.strip("/").split("/") if part] + directory_parts = parts[:-1] if not path.endswith("/") else parts + return any(part.startswith(".") for part in directory_parts) + + +def _insert_tree_path(root: _PathTreeNode, path: str) -> None: + is_directory = path.endswith("/") + parts = [part for part in path.strip("/").split("/") if part] + if not parts: + return + current = root + for directory in parts[:-1]: + current = current.directories.setdefault(directory, _PathTreeNode()) + leaf = parts[-1] + if is_directory: + current.directories.setdefault(leaf, _PathTreeNode()) + else: + current.files.add(leaf) + + +def _render_tree_node(node: _PathTreeNode, *, prefix: str) -> list[str]: + rendered: list[str] = [] + entries: list[tuple[str, str]] = [("directory", name) for name in sorted(node.directories)] + [ + ("file", name) for name in sorted(node.files) + ] + for index, (entry_type, name) in enumerate(entries): + is_last = index == len(entries) - 1 + connector = "└── " if is_last else "├── " + child_prefix = " " if is_last else "│ " + if entry_type == "directory": + rendered.append(f"{prefix}{connector}{name}/") + rendered.extend( + _render_tree_node( + node.directories[name], + prefix=f"{prefix}{child_prefix}", + ) + ) + else: + rendered.append(f"{prefix}{connector}{name}") + return rendered + + def _candidate_keys(explicit_key: str | None, fallback_keys: tuple[str, ...]) -> tuple[str, ...]: if explicit_key is None: return fallback_keys diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/prompt_capabilities.py b/packages/adapters/pydantic-acp/src/pydantic_acp/prompt_capabilities.py new file mode 100644 index 0000000..7b942e5 --- /dev/null +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/prompt_capabilities.py @@ -0,0 +1,12 @@ +from __future__ import annotations as _annotations + +from dataclasses import dataclass + +__all__ = ("AdapterPromptCapabilities",) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class AdapterPromptCapabilities: + audio: bool = True + image: bool = True + embedded_context: bool = True diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_adapter_mixins.py b/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_adapter_mixins.py index ebec189..f7839d1 100644 --- a/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_adapter_mixins.py +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_adapter_mixins.py @@ -111,6 +111,7 @@ def _known_tool_call_starts(self, session: AcpSessionContext) -> dict[str, ToolC def _build_run_kwargs( self, *, + session: AcpSessionContext, message_history: list[ModelMessage] | None, deferred_tool_results: DeferredToolResults | None, deps: AgentDepsT | None, @@ -119,6 +120,7 @@ def _build_run_kwargs( output_type: RunOutputType | None, ) -> dict[str, Any]: return self._prompt_runtime._build_run_kwargs( + session=session, message_history=message_history, deferred_tool_results=deferred_tool_results, deps=deps, diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_adapter_prompt.py b/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_adapter_prompt.py index ac6ffd7..e7d8870 100644 --- a/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_adapter_prompt.py +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_adapter_prompt.py @@ -9,7 +9,9 @@ from acp.schema import AgentMessageChunk, PromptResponse, TextContentBlock from pydantic_ai import Agent as PydanticAgent +from ..awaitables import resolve_value from ..session.state import AcpSessionContext, StoredSessionUpdate, utc_now +from ..slash import SlashCommandRequest, SlashCommandResult from ._prompt_runtime import TaskPlan from .prompts import ( PromptBlock, @@ -73,6 +75,20 @@ async def prompt( slash_response=slash_response, acknowledged_message_id=acknowledged_message_id, ) + custom_slash_response = await self._handle_custom_slash_command( + slash_command.name, + argument=slash_command.argument, + raw_prompt=prompt_text, + session=session, + agent=agent, + ) + if custom_slash_response is not None: + return await self._emit_custom_slash_command_response( + session=session, + agent=agent, + result=custom_slash_response, + acknowledged_message_id=acknowledged_message_id, + ) try: prompt_result = await self._run_prompt( agent=agent, @@ -138,6 +154,71 @@ async def _emit_slash_command_response( user_message_id=acknowledged_message_id, ) + async def _handle_custom_slash_command( + self, + command_name: str, + *, + argument: str | None, + raw_prompt: str, + session: AcpSessionContext, + agent: PydanticAgent[AgentDepsT, OutputDataT], + ) -> SlashCommandResult | None: + provider = self._owner._config.slash_command_provider + if provider is None: + return None + result = await resolve_value( + provider.handle_command( + SlashCommandRequest( + name=command_name, + argument=argument, + raw_prompt=raw_prompt, + session=session, + agent=agent, + ) + ) + ) + if result is None or not result.handled: + return None + return result + + async def _emit_custom_slash_command_response( + self, + *, + session: AcpSessionContext, + agent: PydanticAgent[AgentDepsT, OutputDataT], + result: SlashCommandResult, + acknowledged_message_id: str, + ) -> PromptResponse: + for update in result.updates: + await self._owner._record_update(session, update) + if result.text is not None: + await self._owner._record_update( + session, + AgentMessageChunk( + session_update="agent_message_chunk", + content=TextContentBlock(type="text", text=result.text), + message_id=uuid4().hex, + ), + ) + session.updated_at = utc_now() + self._owner._config.session_store.save(session) + if result.refresh_session_surface: + surface = await self._owner._build_session_surface(session, agent) + await self._owner._emit_session_state_updates( + session, + surface, + emit_available_commands=True, + emit_config_options=True, + emit_current_mode=True, + emit_plan=True, + emit_session_info=True, + ) + return PromptResponse( + stop_reason=result.stop_reason, + usage=None, + user_message_id=acknowledged_message_id, + ) + async def _run_prompt( self, *, diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_prompt_execution.py b/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_prompt_execution.py index 8218077..ce99adf 100644 --- a/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_prompt_execution.py +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_prompt_execution.py @@ -18,7 +18,7 @@ ) from pydantic_ai.tools import DeferredToolRequests, DeferredToolResults -from ..approvals import ApprovalResolution +from ..approvals import ApprovalResolution, supports_projection_aware_approval_bridge from ..projection import ( _is_output_tool, build_compaction_updates, @@ -213,7 +213,37 @@ async def run_prompt_with_events( projection_map = compose_projection_maps(self._runtime._owner._config.projection_maps) streamed_output = False - async for event in agent.run_stream_events(prompt_input, **run_kwargs): + stream_source = agent.run_stream_events(prompt_input, **run_kwargs) + if hasattr(stream_source, "__aenter__"): + async with stream_source as stream: + return await self._consume_run_events( + stream, + known_starts=known_starts, + message_id=message_id, + projection_map=projection_map, + session=session, + streamed_output=streamed_output, + ) + return await self._consume_run_events( + stream_source, + known_starts=known_starts, + message_id=message_id, + projection_map=projection_map, + session=session, + streamed_output=streamed_output, + ) + + async def _consume_run_events( + self, + stream: Any, + *, + known_starts: dict[str, ToolCallStart], + message_id: str, + projection_map: Any, + session: AcpSessionContext, + streamed_output: bool, + ) -> tuple[AgentRunResult[Any], bool]: + async for event in stream: if isinstance(event, AgentRunResultEvent): return event.result, streamed_output if self._runtime._owner._config.enable_generic_tool_projection and isinstance( @@ -286,6 +316,15 @@ async def resolve_deferred_approvals( approval_bridge = self._runtime._owner._config.approval_bridge if approval_bridge is None or self._runtime._owner._client is None: raise RequestError.internal_error({"reason": "deferred_approval_requires_client"}) + projection_map = compose_projection_maps(self._runtime._owner._config.projection_maps) + if supports_projection_aware_approval_bridge(approval_bridge): + return await approval_bridge.resolve_deferred_approvals( + client=self._runtime._owner._client, + session=session, + requests=requests, + classifier=self._runtime._owner._tool_classifier, + projection_map=projection_map, + ) return await approval_bridge.resolve_deferred_approvals( client=self._runtime._owner._client, session=session, @@ -333,6 +372,7 @@ async def _prepare_run_inputs( ) run_output_type = self._runtime._owner._build_run_output_type(agent, session=session) run_kwargs = self._runtime._owner._build_run_kwargs( + session=session, message_history=message_history, deferred_tool_results=deferred_tool_results, deps=deps, diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_prompt_runtime.py b/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_prompt_runtime.py index 06b7cca..b5968e9 100644 --- a/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_prompt_runtime.py +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_prompt_runtime.py @@ -130,6 +130,7 @@ def _known_tool_call_starts(self, session: AcpSessionContext) -> dict[str, ToolC def _build_run_kwargs( self, *, + session: AcpSessionContext, message_history: list[ModelMessage] | None, deferred_tool_results: DeferredToolResults | None, deps: AgentDepsT | None, @@ -140,7 +141,9 @@ def _build_run_kwargs( run_kwargs: dict[str, Any] = { "message_history": message_history, "deferred_tool_results": deferred_tool_results, + "conversation_id": session.session_id, "model": model_override, + "metadata": {"pydantic_acp_session_id": session.session_id}, } if deps is not None: run_kwargs["deps"] = deps diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_session_model_runtime.py b/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_session_model_runtime.py index 03abdc0..65a60e9 100644 --- a/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_session_model_runtime.py +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_session_model_runtime.py @@ -183,5 +183,11 @@ def resolve_unconfigured_model_id(self, model_id: str) -> ModelOverride: from codex_auth_helper import create_codex_responses_model except ImportError as exc: raise RequestError.invalid_params({"modelId": normalized_model_id}) from exc - return create_codex_responses_model(codex_model_id) + return create_codex_responses_model( + codex_model_id, + instructions=( + "Follow the agent instructions already present in the request context " + "and answer consistently with the selected Codex model." + ), + ) return normalized_model_id diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_session_surface_runtime.py b/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_session_surface_runtime.py index 77b1126..2f43dae 100644 --- a/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_session_surface_runtime.py +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/_session_surface_runtime.py @@ -5,11 +5,13 @@ from acp.schema import ( AgentPlanUpdate, + AvailableCommand, AvailableCommandsUpdate, ConfigOptionUpdate, CurrentModeUpdate, PlanEntry, SessionInfoUpdate, + SessionModeState, ) from pydantic_ai import Agent as PydanticAgent from pydantic_ai import models as pydantic_models @@ -29,7 +31,7 @@ build_model_state_from_selection, find_model_option, ) -from .slash_commands import build_available_commands +from .slash_commands import build_available_commands, validate_custom_commands if TYPE_CHECKING: from ._session_runtime import _SessionRuntime @@ -58,6 +60,11 @@ async def build_session_surface( mode_state=mode_state, ) surface = SessionSurface( + available_commands=await self.get_custom_available_commands( + session, + agent, + mode_state=build_mode_state_from_selection(mode_state), + ), config_options=config_options, model_state=build_model_state_from_selection(model_selection_state), mode_state=build_mode_state_from_selection(mode_state), @@ -100,6 +107,7 @@ async def emit_session_state_updates( mode_state=surface.mode_state, model_state=surface.model_state, config_options=surface.config_options, + custom_commands=surface.available_commands, ), ), ) @@ -280,6 +288,20 @@ async def get_provider_config_options( return None return await resolve_value(provider.get_config_options(session, agent)) + async def get_custom_available_commands( + self, + session: AcpSessionContext, + agent: PydanticAgent[AgentDepsT, OutputDataT], + *, + mode_state: SessionModeState | None, + ) -> list[AvailableCommand] | None: + provider = self._runtime._owner._config.slash_command_provider + if provider is None: + return None + commands = list(await resolve_value(provider.available_commands(session, agent))) + validate_custom_commands(commands, mode_state=mode_state) + return commands or None + async def set_provider_config_options( self, session: AcpSessionContext, @@ -326,7 +348,12 @@ async def synchronize_session_metadata( plan_storage = self.plan_storage_metadata(session) if plan_storage is not None: metadata_sections["plan_storage"] = plan_storage - session.metadata = {"pydantic_acp": metadata_sections} if metadata_sections else {} + preserved_metadata = { + key: value for key, value in session.metadata.items() if key != "pydantic_acp" + } + if metadata_sections: + preserved_metadata["pydantic_acp"] = metadata_sections + session.metadata = preserved_metadata async def get_approval_state( self, diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/adapter.py b/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/adapter.py index e7c5a61..906dadf 100644 --- a/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/adapter.py +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/adapter.py @@ -97,9 +97,9 @@ async def initialize( load_session=True, mcp_capabilities=self._bridge_manager.get_mcp_capabilities(), prompt_capabilities=PromptCapabilities( - audio=True, - embedded_context=True, - image=True, + audio=self._config.prompt_capabilities.audio, + embedded_context=self._config.prompt_capabilities.embedded_context, + image=self._config.prompt_capabilities.image, ), session_capabilities=SessionCapabilities( close=SessionCloseCapabilities(), diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/hook_introspection.py b/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/hook_introspection.py index fb6515a..f2f2af6 100644 --- a/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/hook_introspection.py +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/hook_introspection.py @@ -32,13 +32,18 @@ _INTERNAL_EVENT_NAMES = { "_on_event": "event", + "handle_deferred_tool_calls": "deferred_tool_calls", "on_model_request_error": "model_request_error", "on_node_run_error": "node_run_error", + "on_output_process_error": "output_process_error", + "on_output_validate_error": "output_validate_error", "on_run_error": "run_error", "on_tool_execute_error": "tool_execute_error", "on_tool_validate_error": "tool_validate_error", "wrap_model_request": "model_request", "wrap_node_run": "node_run", + "wrap_output_process": "output_process", + "wrap_output_validate": "output_validate", "wrap_run": "run", "wrap_run_event_stream": "run_event_stream", "wrap_tool_execute": "tool_execute", diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/session_surface.py b/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/session_surface.py index 6d9dd1c..28b7257 100644 --- a/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/session_surface.py +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/session_surface.py @@ -6,6 +6,7 @@ from acp.exceptions import RequestError from acp.schema import ( + AvailableCommand, PlanEntry, SessionConfigOptionBoolean, SessionConfigOptionSelect, @@ -36,6 +37,7 @@ class SessionSurface: model_state: SessionModelState | None mode_state: SessionModeState | None plan_entries: list[PlanEntry] | None + available_commands: list[AvailableCommand] | None = None def build_model_config_option( diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/slash_commands.py b/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/slash_commands.py index c019ba6..345d139 100644 --- a/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/slash_commands.py +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/runtime/slash_commands.py @@ -1,5 +1,6 @@ from __future__ import annotations as _annotations +import re from collections.abc import Sequence from dataclasses import dataclass from typing import Any, TypeAlias @@ -24,6 +25,7 @@ HOOKS_COMMAND_NAME, MCP_SERVERS_COMMAND_NAME, MODEL_COMMAND_NAME, + RESERVED_SLASH_COMMAND_NAMES, THINKING_COMMAND_NAME, TOOLS_COMMAND_NAME, validate_mode_command_ids, @@ -47,10 +49,12 @@ "render_model_message", "render_thinking_message", "render_tool_listing", + "validate_custom_commands", "validate_mode_command_ids", ) _INTERNAL_TOOL_PREFIX = "acp_" +_COMMAND_NAME_PATTERN = re.compile(r"^[a-z][a-z0-9-]*$") ConfigOptionType: TypeAlias = SessionConfigOptionSelect | SessionConfigOptionBoolean @@ -80,6 +84,7 @@ def build_available_commands( mode_state: SessionModeState | None, model_state: SessionModelState | None, config_options: Sequence[ConfigOptionType] | None, + custom_commands: Sequence[AvailableCommand] | None = None, ) -> list[AvailableCommand]: commands: list[AvailableCommand] = [] if mode_state is not None: @@ -121,9 +126,53 @@ def build_available_commands( ), ] ) + if custom_commands: + validate_custom_commands(custom_commands, mode_state=mode_state) + commands.extend(custom_commands) return commands +def validate_custom_commands( + commands: Sequence[AvailableCommand], + *, + mode_state: SessionModeState | None, +) -> None: + normalized_names: list[str] = [] + for command in commands: + normalized_name = command.name.strip().lower() + if command.name != normalized_name: + raise ValueError( + f"Slash command name {command.name!r} must already be normalized as " + "a lowercase slash command id." + ) + if not _COMMAND_NAME_PATTERN.fullmatch(normalized_name): + raise ValueError(f"Slash command name {command.name!r} must match ^[a-z][a-z0-9-]*$.") + normalized_names.append(normalized_name) + duplicate_names = sorted( + name for name in set(normalized_names) if normalized_names.count(name) > 1 + ) + if duplicate_names: + raise ValueError( + "Custom slash command names must be unique after normalization. " + f"Duplicate ids: {', '.join(duplicate_names)}." + ) + reserved_names = sorted(set(normalized_names) & RESERVED_SLASH_COMMAND_NAMES) + if reserved_names: + raise ValueError( + "Custom slash command names cannot reuse reserved slash command names " + f"({', '.join(reserved_names)})." + ) + if mode_state is None: + return + mode_ids = {mode.id.strip().lower() for mode in mode_state.available_modes} + conflicting_mode_ids = sorted(set(normalized_names) & mode_ids) + if conflicting_mode_ids: + raise ValueError( + "Custom slash command names cannot reuse active mode ids " + f"({', '.join(conflicting_mode_ids)})." + ) + + def _mode_commands(modes: Sequence[SessionMode]) -> list[AvailableCommand]: return [ AvailableCommand( diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/session/store.py b/packages/adapters/pydantic-acp/src/pydantic_acp/session/store.py index d09877e..d760369 100644 --- a/packages/adapters/pydantic-acp/src/pydantic_acp/session/store.py +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/session/store.py @@ -22,6 +22,10 @@ __all__ = ("FileSessionStore", "MemorySessionStore", "SessionStore") +_MAX_SESSION_ID_LENGTH = 128 +_SESSION_ID_SAFE_CHARS = frozenset( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-" +) _STORE_LOCKS: dict[str, threading.RLock] = {} _STORE_LOCKS_GUARD = threading.Lock() @@ -79,6 +83,7 @@ class FileSessionStore: _lock_path: Path = field(init=False, repr=False) def __post_init__(self) -> None: + self.root = self.root.expanduser().resolve() self.root.mkdir(parents=True, exist_ok=True) self._process_lock = _store_lock(self.root) self._lock_path = self.root / ".acpkit-session-store.lock" @@ -111,6 +116,10 @@ def list_sessions(self) -> list[AcpSessionContext]: with self._locked(): sessions: list[AcpSessionContext] = [] for path in sorted(self.root.glob("*.json")): + try: + _validate_session_id(path.stem) + except ValueError: + continue session = self._load_session_unlocked(path.stem) if session is not None: sessions.append(session) @@ -182,10 +191,12 @@ def _parse_datetime(self, value: str) -> datetime: return datetime.fromisoformat(value) def _session_path(self, session_id: str) -> Path: - return self.root / f"{session_id}.json" + safe_session_id = _validate_session_id(session_id) + return _store_child_path(self.root, f"{safe_session_id}.json") def _temp_session_path(self, session_id: str) -> Path: - return self.root / f".acpkit-session-{session_id}-{uuid4().hex}.tmp" + safe_session_id = _validate_session_id(session_id) + return _store_child_path(self.root, f".acpkit-session-{safe_session_id}-{uuid4().hex}.tmp") def _cleanup_stale_temp_files(self) -> None: for path in self.root.glob(".acpkit-session-*.tmp"): @@ -246,3 +257,21 @@ def _store_lock(root: Path) -> threading.RLock: lock = threading.RLock() _STORE_LOCKS[key] = lock return lock + + +def _validate_session_id(session_id: str) -> str: + if not session_id: + raise ValueError("session_id must not be empty") + if len(session_id) > _MAX_SESSION_ID_LENGTH: + raise ValueError(f"session_id must be at most {_MAX_SESSION_ID_LENGTH} characters") + if any(char not in _SESSION_ID_SAFE_CHARS for char in session_id): + raise ValueError("session_id may only contain ASCII letters, digits, '_' and '-'") + return session_id + + +def _store_child_path(root: Path, filename: str) -> Path: + resolved_root = root.resolve() + path = (resolved_root / filename).resolve() + if path.parent != resolved_root: + raise ValueError("session path must stay inside FileSessionStore root") + return path diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/slash.py b/packages/adapters/pydantic-acp/src/pydantic_acp/slash.py new file mode 100644 index 0000000..fe31878 --- /dev/null +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/slash.py @@ -0,0 +1,84 @@ +from __future__ import annotations as _annotations + +from collections.abc import Awaitable, Callable, Sequence +from dataclasses import dataclass +from typing import Protocol, TypeAlias + +from acp.schema import AvailableCommand, StopReason + +from .agent_types import RuntimeAgent +from .session.state import AcpSessionContext, SessionTranscriptUpdate + +__all__ = ( + "SlashCommandHandler", + "SlashCommandProvider", + "SlashCommandRequest", + "SlashCommandResult", + "StaticSlashCommand", + "StaticSlashCommandProvider", +) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class SlashCommandRequest: + name: str + argument: str | None + raw_prompt: str + session: AcpSessionContext + agent: RuntimeAgent + + +@dataclass(frozen=True, slots=True, kw_only=True) +class SlashCommandResult: + text: str | None = None + updates: Sequence[SessionTranscriptUpdate] = () + stop_reason: StopReason = "end_turn" + handled: bool = True + refresh_session_surface: bool = True + + +class SlashCommandProvider(Protocol): + def available_commands( + self, + session: AcpSessionContext, + agent: RuntimeAgent, + ) -> Sequence[AvailableCommand] | Awaitable[Sequence[AvailableCommand]]: ... + + def handle_command( + self, + request: SlashCommandRequest, + ) -> SlashCommandResult | None | Awaitable[SlashCommandResult | None]: ... + + +SlashCommandHandler: TypeAlias = Callable[ + [SlashCommandRequest], + SlashCommandResult | None | Awaitable[SlashCommandResult | None], +] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class StaticSlashCommand: + command: AvailableCommand + handler: SlashCommandHandler + + +@dataclass(frozen=True, slots=True, kw_only=True) +class StaticSlashCommandProvider: + commands: Sequence[StaticSlashCommand] + + def available_commands( + self, + session: AcpSessionContext, + agent: RuntimeAgent, + ) -> Sequence[AvailableCommand]: + del session, agent + return [command.command for command in self.commands] + + def handle_command( + self, + request: SlashCommandRequest, + ) -> SlashCommandResult | None | Awaitable[SlashCommandResult | None]: + for command in self.commands: + if command.command.name.strip().lower() == request.name: + return command.handler(request) + return None diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/testing/fakes.py b/packages/adapters/pydantic-acp/src/pydantic_acp/testing/fakes.py index 7827c92..4a868d9 100644 --- a/packages/adapters/pydantic-acp/src/pydantic_acp/testing/fakes.py +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/testing/fakes.py @@ -56,6 +56,9 @@ class UpdateRecord: class RecordingACPClient: updates: list[UpdateRecord] = field(default_factory=list) permission_option_ids: list[tuple[str, list[str], ToolCallUpdate]] = field(default_factory=list) + permission_option_names: list[tuple[str, list[str], ToolCallUpdate]] = field( + default_factory=list + ) permission_responses: list[RequestPermissionResponse] = field(default_factory=list) read_calls: list[tuple[str, str, int | None, int | None]] = field(default_factory=list) write_calls: list[tuple[str, str, str]] = field(default_factory=list) @@ -108,6 +111,9 @@ async def request_permission( self.permission_option_ids.append( (session_id, [option.option_id for option in options], tool_call) ) + self.permission_option_names.append( + (session_id, [option.name for option in options], tool_call) + ) if not self.permission_responses: raise AssertionError("unexpected permission request") return self.permission_responses.pop(0) diff --git a/packages/adapters/pydantic-acp/src/pydantic_acp/testing/harness.py b/packages/adapters/pydantic-acp/src/pydantic_acp/testing/harness.py index 665a034..c3867c5 100644 --- a/packages/adapters/pydantic-acp/src/pydantic_acp/testing/harness.py +++ b/packages/adapters/pydantic-acp/src/pydantic_acp/testing/harness.py @@ -6,7 +6,15 @@ from acp import PROTOCOL_VERSION from acp.helpers import text_block from acp.interfaces import Agent as AcpAgent -from acp.schema import HttpMcpServer, McpServerStdio, SseMcpServer, ToolCallProgress, ToolCallStart +from acp.schema import ( + AvailableCommandsUpdate, + HttpMcpServer, + McpServerStdio, + SseMcpServer, + ToolCallProgress, + ToolCallStart, + ToolCallUpdate, +) from pydantic_ai import Agent as PydanticAgent from ..agent_source import AgentFactory, AgentSource @@ -150,6 +158,23 @@ def agent_messages(self, *, session_id: str | None = None) -> list[str]: scoped_client = RecordingACPClient(updates=self.updates(session_id=session_id)) return agent_message_texts(scoped_client) + def available_command_names(self, *, session_id: str | None = None) -> list[str]: + command_updates = self.updates_of_type(AvailableCommandsUpdate, session_id=session_id) + if not command_updates: + return [] + return [command.name for command in command_updates[-1].available_commands] + + def permission_requests(self, *, session_id: str | None = None) -> list[ToolCallUpdate]: + if session_id is None: + return [request[2] for request in self.client.permission_option_ids] + return [ + request[2] for request in self.client.permission_option_ids if request[0] == session_id + ] + + def last_permission_request(self, *, session_id: str | None = None) -> ToolCallUpdate | None: + requests = self.permission_requests(session_id=session_id) + return requests[-1] if requests else None + def tool_updates( self, *, diff --git a/packages/helpers/codex-auth-helper/README.md b/packages/helpers/codex-auth-helper/README.md index dd2ce27..2b251c6 100644 --- a/packages/helpers/codex-auth-helper/README.md +++ b/packages/helpers/codex-auth-helper/README.md @@ -14,7 +14,7 @@ ready-to-use `CodexResponsesModel` or a LangChain chat model. - Reads tokens from `~/.codex/auth.json` - Derives `ChatGPT-Account-Id` from the auth file or token claims - Refreshes expired access tokens with `https://auth.openai.com/oauth/token` -- Writes refreshed tokens back to the auth file +- Writes refreshed tokens back to the auth file with private, atomic file replacement - Builds an OpenAI-compatible client pointed at `https://chatgpt.com/backend-api/codex` - Returns a `pydantic-ai` responses model that already applies the Codex backend requirements - Returns a LangChain `ChatOpenAI` model configured for the Responses API @@ -69,8 +69,11 @@ codex login from codex_auth_helper import create_codex_responses_model from pydantic_ai import Agent -model = create_codex_responses_model("gpt-5.4") -agent = Agent(model, instructions="You are a helpful coding assistant.") +model = create_codex_responses_model( + "gpt-5.4", + instructions="You are a helpful coding assistant.", +) +agent = Agent(model) result = agent.run_sync("Naber") print(result.output) @@ -83,7 +86,10 @@ from codex_auth_helper import create_codex_chat_openai from langchain.agents import create_agent graph = create_agent( - model=create_codex_chat_openai("gpt-5.4"), + model=create_codex_chat_openai( + "gpt-5.4", + instructions="You are a helpful coding assistant.", + ), tools=[], name="codex-graph", ) @@ -95,6 +101,14 @@ The LangChain helper returns `langchain_openai.ChatOpenAI` configured to: - reuse local Codex auth state - keep `use_responses_api=True` - default to `output_version="responses/v1"` +- require `instructions=` and pass it through to the Responses request + +`instructions` is mandatory for `create_codex_chat_openai(...)`. The helper does not provide an +implicit system prompt for the LangChain path; callers must pass the behavior they want explicitly. + +The same rule applies to `create_codex_responses_model(...)` on the Pydantic path. Pass the Codex +system behavior to the helper directly instead of relying on a separate agent-level instruction just +to seed the model. ## Custom Auth Path @@ -106,9 +120,24 @@ from pathlib import Path from codex_auth_helper import CodexAuthConfig, create_codex_responses_model config = CodexAuthConfig(auth_path=Path("/tmp/codex-auth.json")) -model = create_codex_responses_model("gpt-5.4", config=config) +model = create_codex_responses_model( + "gpt-5.4", + config=config, + instructions="You are a helpful coding assistant.", +) ``` +## Auth State Safety + +The auth state file contains credentials and should be treated as private host state. + +When refreshed tokens are written back, `CodexAuthStore` uses a private temp file, `fsync`, atomic +replace, and POSIX `0600` permissions for the final file. If replace fails, the previous auth file is +left intact and the temp file is cleaned up. + +Keep the parent directory private and do not copy auth state into logs, examples, test fixtures, or +container images. + ## Passing Extra OpenAI Responses Settings Additional `OpenAIResponsesModelSettings` can still be passed through. The helper @@ -120,6 +149,7 @@ from codex_auth_helper import create_codex_responses_model model = create_codex_responses_model( "gpt-5.4", + instructions="You are a helpful coding assistant.", settings={ "openai_reasoning_summary": "concise", }, @@ -177,6 +207,7 @@ This package is intentionally small and focused: - auth file parsing - token refresh +- private, atomic auth state writes - Codex-specific OpenAI client wiring - `pydantic-ai` responses model factory - LangChain Responses-model factory @@ -185,3 +216,4 @@ This package is intentionally small and focused: - [Helpers Overview](https://vcoderun.github.io/acpkit/helpers/) - [API Reference](https://vcoderun.github.io/acpkit/api/codex_auth_helper/) +- [Security Guidance](https://vcoderun.github.io/acpkit/security/) diff --git a/packages/helpers/codex-auth-helper/VERSION b/packages/helpers/codex-auth-helper/VERSION index ac39a10..85b7c69 100644 --- a/packages/helpers/codex-auth-helper/VERSION +++ b/packages/helpers/codex-auth-helper/VERSION @@ -1 +1 @@ -0.9.0 +0.9.6 diff --git a/packages/helpers/codex-auth-helper/src/codex_auth_helper/_version.py b/packages/helpers/codex-auth-helper/src/codex_auth_helper/_version.py index bd0f56b..e39e524 100644 --- a/packages/helpers/codex-auth-helper/src/codex_auth_helper/_version.py +++ b/packages/helpers/codex-auth-helper/src/codex_auth_helper/_version.py @@ -2,4 +2,4 @@ __all__ = ("__version__",) -__version__ = "0.9.0" +__version__ = "0.9.6" diff --git a/packages/helpers/codex-auth-helper/src/codex_auth_helper/auth/store.py b/packages/helpers/codex-auth-helper/src/codex_auth_helper/auth/store.py index e5d4e1f..37c5fa6 100644 --- a/packages/helpers/codex-auth-helper/src/codex_auth_helper/auth/store.py +++ b/packages/helpers/codex-auth-helper/src/codex_auth_helper/auth/store.py @@ -1,9 +1,12 @@ from __future__ import annotations as _annotations +import contextlib import json +import os from dataclasses import dataclass from json import JSONDecodeError from pathlib import Path +from uuid import uuid4 from .state import CodexAuthState @@ -34,4 +37,28 @@ def read_state(self) -> CodexAuthState: def write_state(self, state: CodexAuthState) -> None: self.path.parent.mkdir(parents=True, exist_ok=True) encoded = json.dumps(state.to_json_dict(), indent=2) + "\n" - self.path.write_text(encoded, encoding="utf-8") + temp_path = self.path.with_name(f".{self.path.name}.{uuid4().hex}.tmp") + try: + file_descriptor = os.open(temp_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + with os.fdopen(file_descriptor, "w", encoding="utf-8") as temp_file: + temp_file.write(encoded) + temp_file.flush() + os.fsync(temp_file.fileno()) + os.replace(temp_path, self.path) + with contextlib.suppress(OSError): + os.chmod(self.path, 0o600) + _fsync_directory(self.path.parent) + finally: + temp_path.unlink(missing_ok=True) + + +def _fsync_directory(path: Path) -> None: + try: + directory_fd = os.open(path, os.O_RDONLY) + except OSError: + return + try: + with contextlib.suppress(OSError): + os.fsync(directory_fd) + finally: + os.close(directory_fd) diff --git a/packages/helpers/codex-auth-helper/src/codex_auth_helper/factory.py b/packages/helpers/codex-auth-helper/src/codex_auth_helper/factory.py index d702e72..6e4e494 100644 --- a/packages/helpers/codex-auth-helper/src/codex_auth_helper/factory.py +++ b/packages/helpers/codex-auth-helper/src/codex_auth_helper/factory.py @@ -21,8 +21,14 @@ def create_codex_responses_model( *, config: CodexAuthConfig | None = None, http_client: httpx.AsyncClient | None = None, + instructions: str, settings: OpenAIResponsesModelSettings | None = None, ) -> CodexResponsesModel: + if instructions is None: + raise ValueError( + "`instructions` is required for Codex-backed Pydantic models. " + "Pass an explicit system instruction string." + ) client = create_codex_async_openai(config=config, http_client=http_client) model_settings: OpenAIResponsesModelSettings = {"openai_store": False} if settings is not None: @@ -30,6 +36,7 @@ def create_codex_responses_model( model_settings.setdefault("openai_store", False) return CodexResponsesModel( model_name, + default_instructions=instructions, provider=OpenAIProvider(openai_client=client), settings=model_settings, ) @@ -40,6 +47,7 @@ def create_codex_chat_openai( *, config: CodexAuthConfig | None = None, http_client: httpx.AsyncClient | None = None, + instructions: str, sync_http_client: httpx.Client | None = None, include_response_headers: bool = False, model_kwargs: dict[str, Any] | None = None, @@ -66,17 +74,37 @@ def create_codex_chat_openai( async_root_client = create_codex_async_openai(config=config, http_client=http_client) sync_root_client = create_codex_openai(config=config, http_client=sync_http_client) chat_model_kwargs = dict(model_kwargs or {}) + if instructions is None: + raise ValueError( + "`instructions` is required for Codex-backed LangChain models. " + "Pass an explicit system instruction string." + ) + if "store" in chat_model_kwargs: + raise ValueError( + "Do not pass `model_kwargs['store']`; Codex-backed ChatOpenAI always forces " + "`store=False`." + ) + if "instructions" in chat_model_kwargs: + raise ValueError( + "Pass `instructions` either through the dedicated parameter or " + "`model_kwargs['instructions']`, not both." + ) + chat_model_kwargs["instructions"] = instructions + chat_openai_kwargs: dict[str, Any] = { + "model": model_name, + "async_client": async_root_client.chat.completions, + "client": sync_root_client.chat.completions, + "include_response_headers": include_response_headers, + "model_kwargs": chat_model_kwargs, + "output_version": output_version, + "reasoning": reasoning, + "root_async_client": async_root_client, + "root_client": sync_root_client, + "store": False, + "temperature": temperature, + "use_previous_response_id": use_previous_response_id, + "use_responses_api": True, + } return ChatOpenAI( - model_name=model_name, - async_client=async_root_client.chat.completions, - client=sync_root_client.chat.completions, - include_response_headers=include_response_headers, - model_kwargs=chat_model_kwargs, - output_version=output_version, - reasoning=reasoning, - root_async_client=async_root_client, - root_client=sync_root_client, - temperature=temperature, - use_previous_response_id=use_previous_response_id, - use_responses_api=True, + **chat_openai_kwargs, ) diff --git a/packages/helpers/codex-auth-helper/src/codex_auth_helper/model.py b/packages/helpers/codex-auth-helper/src/codex_auth_helper/model.py index 01b3dc7..3144c7a 100644 --- a/packages/helpers/codex-auth-helper/src/codex_auth_helper/model.py +++ b/packages/helpers/codex-auth-helper/src/codex_auth_helper/model.py @@ -1,5 +1,9 @@ from __future__ import annotations as _annotations +from collections.abc import AsyncIterator, Sequence +from contextlib import asynccontextmanager +from typing import Any + from pydantic_ai.messages import ModelRequest, ModelResponse from pydantic_ai.models import ModelRequestParameters from pydantic_ai.models.openai import OpenAIResponsesModel @@ -9,17 +13,62 @@ class CodexResponsesModel(OpenAIResponsesModel): + def __init__( + self, + model_name: str, + *, + default_instructions: str, + provider: Any = "openai", + profile: Any = None, + settings: ModelSettings | None = None, + ) -> None: + self._default_instructions = default_instructions + super().__init__( + model_name, + provider=provider, + profile=profile, + settings=settings, + ) + + def _with_default_instructions( + self, + messages: Sequence[ModelRequest | ModelResponse], + model_request_parameters: ModelRequestParameters, + ) -> list[ModelRequest | ModelResponse]: + resolved = super()._get_instructions(messages, model_request_parameters) + if resolved: + return list(messages) + return [ModelRequest(parts=(), instructions=self._default_instructions), *messages] + async def request( self, messages: list[ModelRequest | ModelResponse], model_settings: ModelSettings | None, model_request_parameters: ModelRequestParameters, ) -> ModelResponse: - async with super().request_stream( - messages, + prepared_messages = self._with_default_instructions(messages, model_request_parameters) + async with self.request_stream( + prepared_messages, model_settings, model_request_parameters, ) as streamed_response: async for _ in streamed_response: pass return streamed_response.get() + + @asynccontextmanager + async def request_stream( + self, + messages: list[ModelRequest | ModelResponse], + model_settings: ModelSettings | None, + model_request_parameters: ModelRequestParameters, + run_context: Any | None = None, + ) -> AsyncIterator[Any]: + prepared_messages = self._with_default_instructions(messages, model_request_parameters) + async with super().request_stream( + prepared_messages, + model_settings, + model_request_parameters, + run_context=run_context, + ) as streamed_response: + yield streamed_response diff --git a/packages/transports/acpremote/README.md b/packages/transports/acpremote/README.md index 1a3eaf6..cba2621 100644 --- a/packages/transports/acpremote/README.md +++ b/packages/transports/acpremote/README.md @@ -52,6 +52,25 @@ await server.serve_forever() keeps command lookup through `PATH` intact while still letting the caller inject tokens or runtime flags. +If you need command cleanup tuning, pass `CommandOptions` to `serve_stdio_command(...)`: + +```python +from acpremote import CommandOptions, serve_stdio_command + +server = await serve_stdio_command( + CommandOptions( + command=('npx', '@zed-industries/codex-acp'), + terminate_timeout=2.0, + ), + host='127.0.0.1', + port=8080, +) +await server.serve_forever() +``` + +When a command-backed WebSocket flow ends, `acpremote` terminates the child process and falls back +to `kill` after `terminate_timeout`. The timeout must be a positive finite number. + Typical remote-host flow: ```bash @@ -134,7 +153,15 @@ Current transport behavior: - binary frames are rejected - bearer-token auth is supported - stdio ACP commands can be mirrored with `serve_command(...)` +- custom command cleanup timeouts are available through `CommandOptions` - transport limits are configurable through `TransportOptions` This package is transport-focused. It doesn't assume ACP Kit adapters or ACP Kit-owned runtime semantics. + +Security guidance: + +- bind to loopback unless a reverse proxy owns TLS and authentication +- allowlist command-backed servers instead of accepting arbitrary command strings +- keep environment overrides minimal and avoid forwarding unnecessary secrets +- see diff --git a/packages/transports/acpremote/VERSION b/packages/transports/acpremote/VERSION index ac39a10..85b7c69 100644 --- a/packages/transports/acpremote/VERSION +++ b/packages/transports/acpremote/VERSION @@ -1 +1 @@ -0.9.0 +0.9.6 diff --git a/packages/transports/acpremote/src/acpremote/_version.py b/packages/transports/acpremote/src/acpremote/_version.py index bd0f56b..e39e524 100644 --- a/packages/transports/acpremote/src/acpremote/_version.py +++ b/packages/transports/acpremote/src/acpremote/_version.py @@ -2,4 +2,4 @@ __all__ = ("__version__",) -__version__ = "0.9.0" +__version__ = "0.9.6" diff --git a/packages/transports/acpremote/src/acpremote/command.py b/packages/transports/acpremote/src/acpremote/command.py index 9e9634c..a4638a3 100644 --- a/packages/transports/acpremote/src/acpremote/command.py +++ b/packages/transports/acpremote/src/acpremote/command.py @@ -2,6 +2,7 @@ import asyncio import contextlib +import math import os import sys from collections.abc import Mapping @@ -22,10 +23,13 @@ class CommandOptions: cwd: str | None = None env: Mapping[str, str] | None = None stderr_mode: Literal["inherit", "discard"] = "inherit" + terminate_timeout: float = 5.0 def __post_init__(self) -> None: if not self.command: raise ValueError("command must not be empty") + if not math.isfinite(self.terminate_timeout) or self.terminate_timeout <= 0: + raise ValueError("terminate_timeout must be a positive finite number") async def run_remote_command_connection( @@ -65,11 +69,15 @@ async def run_remote_command_connection( if process_wait in done or stdout_to_websocket in done: await _close_websocket(websocket) if websocket_to_stdin in done and process.returncode is None: - process.terminate() + await _terminate_process(process, timeout=command_options.terminate_timeout) await _close_stdin(process.stdin) if pending: - done, pending = await asyncio.wait(pending, return_when=asyncio.ALL_COMPLETED) + done, pending = await asyncio.wait( + pending, + timeout=command_options.terminate_timeout, + return_when=asyncio.ALL_COMPLETED, + ) for task in done: _raise_if_needed(task) finally: @@ -81,9 +89,7 @@ async def run_remote_command_connection( await task await _close_stdin(process.stdin) if process.returncode is None: - process.terminate() - with contextlib.suppress(ProcessLookupError): - await process.wait() + await _terminate_process(process, timeout=command_options.terminate_timeout) await _close_websocket(websocket) @@ -154,6 +160,39 @@ async def _close_websocket(websocket: ServerConnection) -> None: await websocket.close() +async def _terminate_process(process: asyncio.subprocess.Process, *, timeout: float) -> None: + try: + if process.returncode is not None: + return + + with contextlib.suppress(ProcessLookupError): + process.terminate() + + try: + await asyncio.wait_for(process.wait(), timeout=timeout) + return + except TimeoutError: + pass + except ProcessLookupError: + return + + if process.returncode is not None: + return + + with contextlib.suppress(ProcessLookupError): + process.kill() + + with contextlib.suppress(ProcessLookupError, TimeoutError): + await asyncio.wait_for(process.wait(), timeout=timeout) + except asyncio.CancelledError: + if process.returncode is None: + with contextlib.suppress(ProcessLookupError): + process.kill() + with contextlib.suppress(ProcessLookupError, asyncio.CancelledError, TimeoutError): + await asyncio.wait_for(asyncio.shield(process.wait()), timeout=timeout) + raise + + def _raise_if_needed(task: asyncio.Task[object]) -> None: try: task.result() diff --git a/pyproject.toml b/pyproject.toml index 4acee20..55ffad0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,8 +16,6 @@ classifiers = [ keywords = ["acp", "agents", "protocol", "pydantic-ai"] dependencies = [ "click>=8.1.8", - "fast-agent-mcp>=0.2.25", - "mcp>=1.27.0", "typing-extensions>=4.12.0", ] @@ -55,7 +53,8 @@ docs = [ "mkdocstrings[python]" ] -all = ["acpkit[codex,deepagents,dev,docs,langchain,launch,pydantic,remote]"] +all = ["acpkit[codex,deepagents,langchain,launch,pydantic,remote]"] +dev-all = ["acpkit[all,dev,docs]"] [project.scripts] acpkit = "acpkit.__main__:main" @@ -77,7 +76,6 @@ members = [ "packages/adapters/*", "packages/helpers/*", "packages/transports/*", - "tmp/acprouter", ] [tool.uv.sources] @@ -135,6 +133,3 @@ addopts = [ "--color=yes", ] markers = [] - -[tool.coverage.run] -omit = ["connect_codex.py"] diff --git a/pyrightconfig.json b/pyrightconfig.json index a759177..fb11c64 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -1,11 +1,12 @@ { "pythonVersion": "3.11", "typeCheckingMode": "standard", - "exclude": ["**/.*", "references/**", "examples/**"], + "exclude": ["**/.*", "references/**", "examples/**", "tests", "acp*"], "extraPaths": ["src"], "reportAny": "none", "reportExplicitAny": "none", "reportMissingImports": "none", "reportPrivateUsage": "none", "reportCallIssue": "none", + "reportPrivateImportUsage": "none", } diff --git a/scripts/generate_llms_docs.py b/scripts/generate_llms_docs.py index 7fff437..2b339d8 100644 --- a/scripts/generate_llms_docs.py +++ b/scripts/generate_llms_docs.py @@ -76,7 +76,7 @@ def url(self) -> str: DocPage( section="Core Docs", title="AdapterConfig", - summary="Field-by-field guide to runtime configuration, ownership, and adapter behavior.", + summary="Field-by-field guide to runtime configuration, prompt capabilities, ownership, and adapter behavior.", path="docs/pydantic-acp/adapter-config.md", ), DocPage( @@ -88,13 +88,13 @@ def url(self) -> str: DocPage( section="Core Docs", title="Models, Modes, and Slash Commands", - summary="Model selection, dynamic mode switching, thinking effort, and slash command semantics.", + summary="Model selection, dynamic mode switching, custom slash commands, thinking effort, and slash command semantics.", path="docs/pydantic-acp/runtime-controls.md", ), DocPage( section="Core Docs", title="Plans, Thinking, and Approvals", - summary="Native plan state, approval flows, cancellation, and thinking capability behavior.", + summary="Native plan state, approval flows, permission presentation, approval policy storage, cancellation, and thinking capability behavior.", path="docs/pydantic-acp/plans-thinking-approvals.md", ), DocPage( @@ -106,19 +106,19 @@ def url(self) -> str: DocPage( section="Core Docs", title="Providers", - summary="Host-owned models, modes, config, plan persistence, and approval metadata patterns.", + summary="Host-owned models, modes, config, plan persistence, approval metadata, and approval policy ownership patterns.", path="docs/providers.md", ), DocPage( section="Core Docs", title="Bridges", - summary="Capability bridges for prepare-tools, thinking, hooks, MCP metadata, and history processors.", + summary="Capability bridges for prepare-tools, thinking, hooks, external hook events, MCP metadata, and history processors.", path="docs/bridges.md", ), DocPage( section="Core Docs", title="Host Backends and Projections", - summary="Client-backed filesystem, terminal execution, and projection map rendering.", + summary="Client-backed filesystem, terminal execution, projection map rendering, search/list projection, and tool classification.", path="docs/host-backends.md", ), DocPage( @@ -238,7 +238,7 @@ def url(self) -> str: DocPage( section="API Reference", title="pydantic_acp API", - summary="API reference for the adapter package, session stores, providers, bridges, and helpers.", + summary="API reference for the adapter package, session stores, providers, bridges, approval policy/presentation seams, slash commands, projections, and helpers.", path="docs/api/pydantic_acp.md", ), DocPage( diff --git a/src/acpkit/_version.py b/src/acpkit/_version.py index bd0f56b..e39e524 100644 --- a/src/acpkit/_version.py +++ b/src/acpkit/_version.py @@ -2,4 +2,4 @@ __all__ = ("__version__",) -__version__ = "0.9.0" +__version__ = "0.9.6" diff --git a/src/acpkit/cli.py b/src/acpkit/cli.py index 94cec23..bec4d1f 100644 --- a/src/acpkit/cli.py +++ b/src/acpkit/cli.py @@ -154,7 +154,7 @@ def main(argv: Sequence[str] | None = None) -> int: standalone_mode=False, ) except click.ClickException as exc: - exc.show(file=sys.stderr) + click.echo(f"Error: {exc.format_message()}", file=sys.stderr, err=True) return exc.exit_code except click.exceptions.Exit as exc: return exc.exit_code diff --git a/tests/acpremote/test_helpers.py b/tests/acpremote/test_helpers.py index bee31b1..81a92a7 100644 --- a/tests/acpremote/test_helpers.py +++ b/tests/acpremote/test_helpers.py @@ -191,6 +191,32 @@ async def readline(self) -> bytes: return self.lines.pop(0) +@dataclass(slots=True) +class _FakeCommandProcess: + stdin: Any = field(default_factory=_FakeStdin) + stdout: Any = field(default_factory=object) + returncode: int | None = None + terminate_calls: int = 0 + kill_calls: int = 0 + wait_delay: float = 0.0 + wait_error: Exception | None = None + + def terminate(self) -> None: + self.terminate_calls += 1 + + def kill(self) -> None: + self.kill_calls += 1 + self.returncode = -9 + + async def wait(self) -> int: + await asyncio.sleep(self.wait_delay) + if self.wait_error is not None: + raise self.wait_error + if self.returncode is None: # pragma: no branch + self.returncode = 0 + return self.returncode + + @dataclass(slots=True) class _FakeCommandWebSocket: messages: list[str | bytes] @@ -515,26 +541,6 @@ async def fake_close_stdin(stdin: Any) -> None: monkeypatch.setattr(command_module, "_close_websocket", fake_close_websocket) monkeypatch.setattr(command_module, "_close_stdin", fake_close_stdin) - @dataclass(slots=True) - class _FakeProcess: - stdin: Any = field(default_factory=object) - stdout: Any = field(default_factory=object) - returncode: int | None = None - terminate_calls: int = 0 - wait_delay: float = 0.0 - wait_error: Exception | None = None - - def terminate(self) -> None: - self.terminate_calls += 1 - - async def wait(self) -> int: - await asyncio.sleep(self.wait_delay) - if self.wait_error is not None: - raise self.wait_error - if self.returncode is None: # pragma: no branch - self.returncode = 0 - return self.returncode - async def done_immediately(*args: Any, **kwargs: Any) -> None: del args, kwargs return None @@ -543,9 +549,9 @@ async def done_later(*args: Any, **kwargs: Any) -> None: del args, kwargs await asyncio.sleep(0.01) - fast_exit = _FakeProcess(wait_delay=0.0) + fast_exit = _FakeCommandProcess(wait_delay=0.0) - async def create_fast_process(**kwargs: Any) -> _FakeProcess: + async def create_fast_process(**kwargs: Any) -> _FakeCommandProcess: del kwargs return fast_exit @@ -560,9 +566,9 @@ async def create_fast_process(**kwargs: Any) -> _FakeProcess: close_calls.clear() stdin_close_calls.clear() - delayed_exit = _FakeProcess(wait_delay=0.01, wait_error=ProcessLookupError()) + delayed_exit = _FakeCommandProcess(wait_delay=0.01, wait_error=ProcessLookupError()) - async def create_delayed_process(**kwargs: Any) -> _FakeProcess: + async def create_delayed_process(**kwargs: Any) -> _FakeCommandProcess: del kwargs return delayed_exit @@ -577,27 +583,6 @@ async def create_delayed_process(**kwargs: Any) -> _FakeProcess: assert close_calls assert stdin_close_calls - close_calls.clear() - stdin_close_calls.clear() - - async def all_done(*args: Any, **kwargs: Any) -> None: - del args, kwargs - return None - - instant_exit = _FakeProcess(wait_delay=0.0) - - async def create_instant_process(**kwargs: Any) -> _FakeProcess: - del kwargs - return instant_exit - - monkeypatch.setattr(command_module, "_create_command_process", create_instant_process) - monkeypatch.setattr(command_module, "_relay_websocket_to_stdin", all_done) - monkeypatch.setattr(command_module, "_relay_stdout_to_websocket", all_done) - await command_module.run_remote_command_connection( - cast(Any, object()), - command_options=command_module.CommandOptions(command=("echo", "hi")), - ) - async def failing_relay(*args: Any, **kwargs: Any) -> None: del args, kwargs raise ValueError("boom") @@ -606,9 +591,9 @@ async def slow_relay(*args: Any, **kwargs: Any) -> None: del args, kwargs await asyncio.sleep(0.05) - hanging_exit = _FakeProcess(wait_delay=0.05) + hanging_exit = _FakeCommandProcess(wait_delay=0.05) - async def create_hanging_process(**kwargs: Any) -> _FakeProcess: + async def create_hanging_process(**kwargs: Any) -> _FakeCommandProcess: del kwargs return hanging_exit @@ -618,8 +603,126 @@ async def create_hanging_process(**kwargs: Any) -> _FakeProcess: with pytest.raises(ValueError, match="boom"): await command_module.run_remote_command_connection( cast(Any, object()), - command_options=command_module.CommandOptions(command=("echo", "hi")), + command_options=command_module.CommandOptions( + command=("echo", "hi"), + terminate_timeout=0.001, + ), ) + assert hanging_exit.kill_calls >= 1 + + with pytest.raises(ValueError, match="terminate_timeout"): + command_module.CommandOptions(command=("echo",), terminate_timeout=0) + + +@pytest.mark.asyncio +async def test_command_process_terminate_timeout_kills() -> None: + process = _FakeCommandProcess(wait_delay=0.02) + + await command_module._terminate_process(cast(Any, process), timeout=0.001) + + assert process.terminate_calls == 1 + assert process.kill_calls == 1 + assert process.returncode == -9 + + +@pytest.mark.asyncio +async def test_command_process_terminate_returns_when_already_exited() -> None: + process = _FakeCommandProcess(returncode=0) + + await command_module._terminate_process(cast(Any, process), timeout=0.001) + + assert process.terminate_calls == 0 + assert process.kill_calls == 0 + + +@pytest.mark.asyncio +async def test_command_process_terminate_returns_when_timeout_sets_returncode() -> None: + @dataclass(slots=True) + class _ExitingOnCancelledProcess(_FakeCommandProcess): + async def wait(self) -> int: + try: + await asyncio.sleep(1) + except asyncio.CancelledError: + self.returncode = 143 + raise + return self.returncode or 0 # pragma: no cover + + process = _ExitingOnCancelledProcess() + + await command_module._terminate_process(cast(Any, process), timeout=0.001) + + assert process.terminate_calls == 1 + assert process.kill_calls == 0 + assert process.returncode == 143 + + +@pytest.mark.asyncio +async def test_command_process_terminate_kills_when_cancelled() -> None: + process = _FakeCommandProcess(wait_delay=0.01) + task = asyncio.create_task( + command_module._terminate_process(cast(Any, process), timeout=1), + ) + + await asyncio.sleep(0) + task.cancel() + with pytest.raises(asyncio.CancelledError): + await task + + assert process.terminate_calls == 1 + assert process.kill_calls == 1 + + +@pytest.mark.asyncio +async def test_command_process_terminate_cancelled_after_process_exit() -> None: + @dataclass(slots=True) + class _ExitedOnCancelledProcess(_FakeCommandProcess): + async def wait(self) -> int: + try: + await asyncio.sleep(1) + except asyncio.CancelledError: + self.returncode = 0 + raise + return self.returncode or 0 # pragma: no cover + + process = _ExitedOnCancelledProcess() + task = asyncio.create_task( + command_module._terminate_process(cast(Any, process), timeout=1), + ) + + await asyncio.sleep(0) + task.cancel() + with pytest.raises(asyncio.CancelledError): + await task + + assert process.terminate_calls == 1 + assert process.kill_calls == 0 + assert process.returncode == 0 + + +@pytest.mark.asyncio +async def test_command_connection_covers_all_done_path( + monkeypatch: pytest.MonkeyPatch, +) -> None: + async def all_done(*args: Any, **kwargs: Any) -> None: + del args, kwargs + return None + + process = _FakeCommandProcess(wait_delay=0.0) + + async def create_process(**kwargs: Any) -> _FakeCommandProcess: + del kwargs + return process + + monkeypatch.setattr(command_module, "_create_command_process", create_process) + monkeypatch.setattr(command_module, "_relay_websocket_to_stdin", all_done) + monkeypatch.setattr(command_module, "_relay_stdout_to_websocket", all_done) + + await command_module.run_remote_command_connection( + cast(Any, _FakeCommandWebSocket(messages=[])), + command_options=command_module.CommandOptions(command=("echo", "hi")), + ) + + assert process.returncode == 0 @pytest.mark.asyncio diff --git a/tests/codex_auth_helper/test_auth.py b/tests/codex_auth_helper/test_auth.py index 604a0d9..18712aa 100644 --- a/tests/codex_auth_helper/test_auth.py +++ b/tests/codex_auth_helper/test_auth.py @@ -2,6 +2,7 @@ import asyncio import json +import stat from datetime import UTC, datetime, timedelta from pathlib import Path from typing import Any, cast @@ -22,7 +23,7 @@ _parse_timestamp, _require_str, ) -from codex_auth_helper.auth.store import CodexAuthStore +from codex_auth_helper.auth.store import CodexAuthStore, _fsync_directory from .support import write_auth_file @@ -177,6 +178,33 @@ def test_auth_store_covers_invalid_json_non_object_and_write_round_trip( store.write_state(updated_state) persisted = json.loads(auth_path.read_text(encoding="utf-8")) assert persisted["tokens"]["account_id"] == "acct_written" + assert stat.S_IMODE(auth_path.stat().st_mode) == 0o600 + + failed_state = CodexAuthState( + access_token=state.access_token, + refresh_token=state.refresh_token, + account_id="acct_failed", + auth_mode="oauth", + last_refresh=datetime.now(tz=UTC), + ) + with pytest.MonkeyPatch.context() as monkeypatch: + monkeypatch.setattr( + "codex_auth_helper.auth.store.os.replace", + lambda *_args, **_kwargs: (_ for _ in ()).throw(OSError("replace failed")), + ) + with pytest.raises(OSError, match="replace failed"): + store.write_state(failed_state) + + persisted_after_failure = json.loads(auth_path.read_text(encoding="utf-8")) + assert persisted_after_failure["tokens"]["account_id"] == "acct_written" + assert not list(auth_path.parent.glob(f".{auth_path.name}.*.tmp")) + + with pytest.MonkeyPatch.context() as monkeypatch: + monkeypatch.setattr( + "codex_auth_helper.auth.store.os.open", + lambda *_args, **_kwargs: (_ for _ in ()).throw(OSError("open failed")), + ) + _fsync_directory(auth_path.parent) @pytest.mark.asyncio diff --git a/tests/codex_auth_helper/test_factory.py b/tests/codex_auth_helper/test_factory.py index 13a60c3..c358944 100644 --- a/tests/codex_auth_helper/test_factory.py +++ b/tests/codex_auth_helper/test_factory.py @@ -4,7 +4,7 @@ import builtins from datetime import UTC, datetime, timedelta from pathlib import Path -from typing import Any +from typing import Any, cast import httpx import pytest @@ -41,7 +41,11 @@ def test_create_codex_responses_model_returns_openai_responses_model( auth_path = tmp_path / "auth.json" write_auth_file(auth_path, account_id="acct_demo") - model = create_codex_responses_model("gpt-5", config=_config(auth_path)) + model = create_codex_responses_model( + "gpt-5", + config=_config(auth_path), + instructions="Answer tersely.", + ) assert isinstance(model, OpenAIResponsesModel) assert isinstance(model, CodexResponsesModel) @@ -58,6 +62,7 @@ def test_create_codex_responses_model_merges_settings(tmp_path: Path) -> None: model = create_codex_responses_model( "gpt-5", config=_config(auth_path), + instructions="Answer tersely.", settings={"openai_reasoning_summary": "concise"}, ) @@ -67,13 +72,31 @@ def test_create_codex_responses_model_merges_settings(tmp_path: Path) -> None: } +def test_create_codex_responses_model_rejects_missing_instructions_runtime( + tmp_path: Path, +) -> None: + auth_path = tmp_path / "auth.json" + write_auth_file(auth_path, account_id="acct_demo") + + with pytest.raises(ValueError, match="`instructions` is required"): + create_codex_responses_model( + "gpt-5", + config=_config(auth_path), + instructions=cast(Any, None), + ) + + @pytest.mark.asyncio async def test_codex_responses_model_forces_streaming_on_request( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: auth_path = tmp_path / "auth.json" write_auth_file(auth_path, account_id="acct_demo") - model = create_codex_responses_model("gpt-5", config=_config(auth_path)) + model = create_codex_responses_model( + "gpt-5", + config=_config(auth_path), + instructions="Answer tersely.", + ) expected_response = ModelResponse(parts=[TextPart("ok")], model_name="gpt-5") seen_stream_values: list[bool] = [] @@ -267,6 +290,7 @@ def test_create_codex_chat_openai_returns_langchain_chat_model( model = create_codex_chat_openai( "gpt-5", config=_config(auth_path), + instructions="Answer tersely.", reasoning={"effort": "medium"}, use_previous_response_id=True, ) @@ -278,12 +302,44 @@ def test_create_codex_chat_openai_returns_langchain_chat_model( assert model.output_version == "responses/v1" assert model.use_previous_response_id is True assert model.reasoning == {"effort": "medium"} + assert model.model_kwargs["instructions"] == "Answer tersely." + assert model.store is False assert model.root_async_client.token_manager.current_account_id == "acct_langchain" model.root_client.close() asyncio.run(model.root_async_client.close()) +def test_create_codex_chat_openai_rejects_duplicate_instructions( + tmp_path: Path, +) -> None: + auth_path = tmp_path / "auth.json" + write_auth_file(auth_path, account_id="acct_langchain") + + with pytest.raises(ValueError, match="dedicated parameter"): + create_codex_chat_openai( + "gpt-5", + config=_config(auth_path), + instructions="Answer tersely.", + model_kwargs={"instructions": "Be concise."}, + ) + + +def test_create_codex_chat_openai_rejects_store_override( + tmp_path: Path, +) -> None: + auth_path = tmp_path / "auth.json" + write_auth_file(auth_path, account_id="acct_langchain") + + with pytest.raises(ValueError, match="always forces `store=False`"): + create_codex_chat_openai( + "gpt-5", + config=_config(auth_path), + instructions="Answer tersely.", + model_kwargs={"store": True}, + ) + + def test_create_codex_chat_openai_reports_missing_optional_dependency( monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -304,7 +360,21 @@ def fake_import( monkeypatch.setattr(builtins, "__import__", fake_import) with pytest.raises(ModuleNotFoundError, match="codex-auth-helper\\[langchain\\]"): - create_codex_chat_openai("gpt-5") + create_codex_chat_openai("gpt-5", instructions="Answer tersely.") + + +def test_create_codex_chat_openai_rejects_missing_instructions_runtime( + tmp_path: Path, +) -> None: + auth_path = tmp_path / "auth.json" + write_auth_file(auth_path, account_id="acct_langchain") + + with pytest.raises(ValueError, match="`instructions` is required"): + create_codex_chat_openai( + "gpt-5", + config=_config(auth_path), + instructions=cast(Any, None), + ) @pytest.mark.asyncio diff --git a/tests/langchain/test_examples.py b/tests/langchain/test_examples.py index 5b0deed..19a7c2a 100644 --- a/tests/langchain/test_examples.py +++ b/tests/langchain/test_examples.py @@ -1,6 +1,9 @@ from __future__ import annotations as _annotations +import importlib import runpy +import sys +from itertools import cycle from pathlib import Path from types import SimpleNamespace from typing import Any, cast @@ -8,13 +11,38 @@ import pytest from langchain_acp import AcpSessionContext from langchain_acp.session import utc_now +from langchain_core.language_models import GenericFakeChatModel +from langchain_core.messages import AIMessage -from examples.langchain import codex_graph, deepagents_graph, workspace_graph + +def _fake_codex_model() -> GenericFakeChatModel: + return GenericFakeChatModel(messages=cycle([AIMessage(content="codex-ready")])) + + +def _load_example_module( + monkeypatch: pytest.MonkeyPatch, + module_name: str, +) -> Any: + import codex_auth_helper + + monkeypatch.setattr( + codex_auth_helper, + "create_codex_chat_openai", + lambda _model_name, **_kwargs: _fake_codex_model(), + ) + sys.modules.pop(module_name, None) + return importlib.import_module(module_name) + + +def _run_example_module_as_main(module_name: str) -> dict[str, Any]: + sys.modules.pop(module_name, None) + return runpy.run_module(module_name, run_name="__main__") def test_langchain_example_main_dispatches_run_acp( monkeypatch: pytest.MonkeyPatch, ) -> None: + workspace_graph = _load_example_module(monkeypatch, "examples.langchain.workspace_graph") captured: list[tuple[Any, Any]] = [] def fake_run_acp(*, graph_factory: Any, config: Any) -> None: @@ -29,10 +57,12 @@ def fake_run_acp(*, graph_factory: Any, config: Any) -> None: def test_codex_langchain_example_builds_graph_from_helper( monkeypatch: pytest.MonkeyPatch, ) -> None: + codex_graph = _load_example_module(monkeypatch, "examples.langchain.codex_graph") captured: dict[str, Any] = {} - def fake_create_codex_chat_openai(model_name: str) -> str: + def fake_create_codex_chat_openai(model_name: str, *, instructions: str) -> str: captured["model_name"] = model_name + captured["instructions"] = instructions return "codex-model" def fake_create_agent(*, model: Any, tools: list[Any], name: str) -> object: @@ -50,25 +80,27 @@ def fake_create_agent(*, model: Any, tools: list[Any], name: str) -> object: assert captured["model_name"] == codex_graph.MODEL_NAME assert captured["model"] == "codex-model" assert captured["name"] == "codex-graph" + assert "workspace assistant" in captured["instructions"] assert [tool.__name__ for tool in captured["tools"]] == ["describe_codex_surface"] assert "Codex graph features:" in codex_graph.describe_codex_surface() + assert codex_graph.config.available_models + assert codex_graph.config.available_modes def test_codex_langchain_example_main_dispatches_run_acp( monkeypatch: pytest.MonkeyPatch, ) -> None: + codex_graph = _load_example_module(monkeypatch, "examples.langchain.codex_graph") captured: list[tuple[Any, Any]] = [] - monkeypatch.setattr(codex_graph, "build_graph", lambda: "graph-object") - - def fake_run_acp(*, graph: Any, config: Any) -> None: - captured.append((graph, config)) + def fake_run_acp(*, graph_factory: Any, config: Any) -> None: + captured.append((graph_factory, config)) monkeypatch.setattr(codex_graph, "run_acp", fake_run_acp) codex_graph.main() - assert captured == [("graph-object", codex_graph.config)] + assert captured == [(codex_graph.graph_from_session, codex_graph.config)] def test_codex_langchain_example_module_runs_as_main( @@ -80,7 +112,11 @@ def test_codex_langchain_example_module_runs_as_main( import langchain.agents import langchain_acp - monkeypatch.setattr(codex_auth_helper, "create_codex_chat_openai", lambda _: "codex-model") + monkeypatch.setattr( + codex_auth_helper, + "create_codex_chat_openai", + lambda _, **_kwargs: "codex-model", + ) monkeypatch.setattr( langchain.agents, "create_agent", @@ -91,23 +127,75 @@ def test_codex_langchain_example_module_runs_as_main( }, ) - def fake_run_acp(*, graph: Any, config: Any) -> None: - observed["call"] = (graph, config) + def fake_run_acp(*, graph_factory: Any, config: Any) -> None: + observed["call"] = (graph_factory, config) monkeypatch.setattr(langchain_acp, "run_acp", fake_run_acp) - runpy.run_module("examples.langchain.codex_graph", run_name="__main__") + _run_example_module_as_main("examples.langchain.codex_graph") - graph, config = observed["call"] + graph_factory, config = observed["call"] + session = AcpSessionContext( + session_id="codex-example", + cwd=Path.cwd(), + created_at=utc_now(), + updated_at=utc_now(), + ) + graph = graph_factory(session) assert graph["model"] == "codex-model" - assert graph["name"] == "codex-graph" + assert graph["name"].startswith("codex-") assert config is not None +def test_codex_langchain_example_graph_factory_uses_session_model_and_mode( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + codex_graph = _load_example_module(monkeypatch, "examples.langchain.codex_graph") + captured: dict[str, Any] = {} + + def fake_create_codex_chat_openai(model_name: str, *, instructions: str) -> str: + captured["model_name"] = model_name + captured["instructions"] = instructions + return "codex-model" + + def fake_create_agent(*, model: Any, tools: list[Any], name: str) -> object: + captured["model"] = model + captured["name"] = name + captured["tools"] = tools + return {"model": model, "name": name} + + monkeypatch.setattr(codex_graph, "create_codex_chat_openai", fake_create_codex_chat_openai) + monkeypatch.setattr(codex_graph, "create_agent", fake_create_agent) + + session = AcpSessionContext( + session_id="codex-session", + cwd=tmp_path, + created_at=utc_now(), + updated_at=utc_now(), + session_model_id="gpt-5.4", + session_mode_id="plan", + ) + graph = codex_graph.graph_from_session(session) + + assert graph == {"model": "codex-model", "name": f"codex-plan-{tmp_path.name}"} + assert captured["model_name"] == "gpt-5.4" + assert "short plan" in captured["instructions"] + + +def test_codex_langchain_example_edit_mode_instructions( + monkeypatch: pytest.MonkeyPatch, +) -> None: + codex_graph = _load_example_module(monkeypatch, "examples.langchain.codex_graph") + + assert "changed files" in codex_graph.codex_instructions(mode_id="edit") + + def test_langchain_example_workspace_helpers_cover_seeded_paths( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: + workspace_graph = _load_example_module(monkeypatch, "examples.langchain.workspace_graph") root = tmp_path / ".workspace-graph" monkeypatch.setattr(workspace_graph, "WORKSPACE_ROOT", root) @@ -118,6 +206,8 @@ def test_langchain_example_workspace_helpers_cover_seeded_paths( assert "Workspace Graph Demo" in workspace_graph.read_workspace_note("README.md") assert workspace_graph.write_workspace_note("scratch.txt", "# Hello") == "Wrote `scratch.txt`." assert workspace_graph.list_workspace_files() == "README.md\nscratch.txt" + assert workspace_graph.config.available_models + assert workspace_graph.config.available_modes with pytest.raises(ValueError, match="workspace graph demo directory"): workspace_graph._resolve_workspace_path("../escape.md") @@ -130,7 +220,9 @@ def test_langchain_example_workspace_helpers_cover_seeded_paths( def test_langchain_example_workspace_graph_factory_uses_session_root( tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, ) -> None: + workspace_graph = _load_example_module(monkeypatch, "examples.langchain.workspace_graph") session_root = tmp_path / "remote-workspace" session_root.mkdir(parents=True, exist_ok=True) session = AcpSessionContext( @@ -147,17 +239,63 @@ def test_langchain_example_workspace_graph_factory_uses_session_root( assert (seeded_root / "README.md").exists() +def test_langchain_example_workspace_graph_factory_uses_session_model_and_mode( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + workspace_graph = _load_example_module(monkeypatch, "examples.langchain.workspace_graph") + captured: dict[str, Any] = {} + + def fake_create_codex_chat_openai(model_name: str, *, instructions: str) -> str: + captured["model_name"] = model_name + captured["instructions"] = instructions + return "workspace-model" + + def fake_create_agent(*, model: Any, tools: list[Any], name: str) -> object: + captured["model"] = model + captured["name"] = name + captured["tools"] = tools + return {"model": model, "name": name} + + monkeypatch.setattr(workspace_graph, "create_codex_chat_openai", fake_create_codex_chat_openai) + monkeypatch.setattr(workspace_graph, "create_agent", fake_create_agent) + + session = AcpSessionContext( + session_id="workspace-session", + cwd=tmp_path, + created_at=utc_now(), + updated_at=utc_now(), + session_model_id="gpt-5.4", + session_mode_id="edit", + ) + graph = workspace_graph.graph_from_session(session) + + assert graph == {"model": "workspace-model", "name": f"workspace-edit-{tmp_path.name}"} + assert captured["model_name"] == "gpt-5.4" + assert "smallest viable file update" in captured["instructions"] + + +def test_langchain_example_workspace_plan_mode_instructions( + monkeypatch: pytest.MonkeyPatch, +) -> None: + workspace_graph = _load_example_module(monkeypatch, "examples.langchain.workspace_graph") + + assert "short implementation plan" in workspace_graph.codex_instructions(mode_id="plan") + + def test_langchain_example_workspace_bound_tools_cover_private_closures( tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, ) -> None: + workspace_graph = _load_example_module(monkeypatch, "examples.langchain.workspace_graph") root = tmp_path / ".workspace-graph" root.mkdir(parents=True, exist_ok=True) - tools = {cast(Any, tool).__name__: tool for tool in workspace_graph._bind_workspace_tools(root)} + tools = {tool.__name__: tool for tool in workspace_graph._bind_workspace_tools(root)} assert tools["describe_workspace_surface"]() == workspace_graph.describe_workspace_surface() assert tools["list_workspace_files"]() == "" - assert cast(Any, tools["read_workspace_note"]).__name__ == "read_workspace_note" - assert cast(Any, tools["write_workspace_note"]).__name__ == "write_workspace_note" + assert tools["read_workspace_note"].__name__ == "read_workspace_note" + assert tools["write_workspace_note"].__name__ == "write_workspace_note" assert tools["write_workspace_note"]("note.md", "# Demo") == "Wrote `note.md`." assert tools["list_workspace_files"]() == "README.md\nnote.md" @@ -178,8 +316,15 @@ def fake_run_acp(*, graph_factory: Any, config: Any) -> None: observed["call"] = (graph_factory, config) monkeypatch.setattr(langchain_acp, "run_acp", fake_run_acp) + import codex_auth_helper - runpy.run_module("examples.langchain.workspace_graph", run_name="__main__") + monkeypatch.setattr( + codex_auth_helper, + "create_codex_chat_openai", + lambda _model_name, **_kwargs: _fake_codex_model(), + ) + + _run_example_module_as_main("examples.langchain.workspace_graph") graph_factory, config = observed["call"] assert graph_factory.__name__ == "graph_from_session" @@ -189,6 +334,7 @@ def fake_run_acp(*, graph_factory: Any, config: Any) -> None: def test_deepagents_example_main_dispatches_run_acp( monkeypatch: pytest.MonkeyPatch, ) -> None: + deepagents_graph = _load_example_module(monkeypatch, "examples.langchain.deepagents_graph") captured: list[tuple[Any, Any]] = [] def fake_run_acp(*, graph_factory: Any, config: Any) -> None: @@ -204,20 +350,40 @@ def test_deepagents_example_workspace_helpers_cover_seeded_paths( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: + deepagents_graph = _load_example_module(monkeypatch, "examples.langchain.deepagents_graph") root = tmp_path / ".deepagents-graph" monkeypatch.setattr(deepagents_graph, "WORKSPACE_ROOT", root) deepagents_graph._ensure_workspace() - assert deepagents_graph.list_workspace_files() == "brief.md" + assert deepagents_graph.list_workspace_files() == "brief.md\nnotes.md" assert "DeepAgents Demo" in deepagents_graph.read_file("brief.md") - assert deepagents_graph.write_file("itinerary.md", "# Trip") == "Wrote itinerary.md" - assert deepagents_graph.list_workspace_files() == "brief.md\nitinerary.md" - assert deepagents_graph.read_file("itinerary.md") == "# Trip" + assert ( + deepagents_graph.write_file("itinerary.md", "# Trip") + == "Mock write accepted for itinerary.md" + ) + assert deepagents_graph.list_workspace_files() == "brief.md\nnotes.md" + assert deepagents_graph.config.available_models + assert deepagents_graph.config.available_modes assert ( deepagents_graph._resolve_workspace_path("itinerary.md", root=root) == (root / "itinerary.md").resolve() ) + assert ( + deepagents_graph._resolve_workspace_path(".deepagents-graph/itinerary.md", root=root) + == (root / "itinerary.md").resolve() + ) + assert ( + deepagents_graph._resolve_workspace_path(str((root / "itinerary.md").resolve()), root=root) + == (root / "itinerary.md").resolve() + ) + assert ( + deepagents_graph._resolve_workspace_path( + str((root.parent / "itinerary.md").resolve()), + root=root, + ) + == (root / "itinerary.md").resolve() + ) with pytest.raises(ValueError, match="DeepAgents example workspace"): deepagents_graph._resolve_workspace_path("../escape.md") @@ -228,9 +394,38 @@ def test_deepagents_example_workspace_helpers_cover_seeded_paths( assert deepagents_graph.config.projection_maps +def test_deepagents_example_helper_edges_cover_remaining_branches( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + deepagents_graph = _load_example_module(monkeypatch, "examples.langchain.deepagents_graph") + root = tmp_path / ".deepagents-graph" + monkeypatch.setattr(deepagents_graph, "WORKSPACE_ROOT", root) + + assert "direct file edits" in deepagents_graph.codex_instructions(mode_id="edit") + + outside_root = tmp_path / "outside" + outside_root.mkdir(parents=True, exist_ok=True) + outside_child = outside_root / "escape.md" + resolved = deepagents_graph._resolve_workspace_path(str(outside_child.resolve()), root=root) + assert resolved == (root / "outside" / "escape.md").resolve() + + nested_prefixed = root.parent / root.name / "nested" / "note.md" + normalized = deepagents_graph._resolve_workspace_path(str(nested_prefixed.resolve()), root=root) + assert normalized == (root / "nested" / "note.md").resolve() + + external_absolute = Path("/tmp") / "deepagents-escape.md" + with pytest.raises(ValueError, match="DeepAgents example workspace"): + deepagents_graph._resolve_workspace_path(str(external_absolute.resolve()), root=root) + + with pytest.raises(ValueError, match="mocked workspace file"): + deepagents_graph._normalized_mock_path("") + + def test_deepagents_example_graph_factory_requires_optional_dependency( monkeypatch: pytest.MonkeyPatch, ) -> None: + deepagents_graph = _load_example_module(monkeypatch, "examples.langchain.deepagents_graph") session = AcpSessionContext( session_id="example-session", cwd=Path.cwd(), @@ -248,6 +443,7 @@ def test_deepagents_example_graph_factory_builds_graph_from_lazy_import( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: + deepagents_graph = _load_example_module(monkeypatch, "examples.langchain.deepagents_graph") root = tmp_path / ".deepagents-graph" monkeypatch.setattr(deepagents_graph, "WORKSPACE_ROOT", root) monkeypatch.setattr(deepagents_graph, "_deepagents_available", lambda: True) @@ -258,10 +454,10 @@ def fake_create_deep_agent(**kwargs: Any) -> object: captured.update(kwargs) return object() - monkeypatch.setattr( - deepagents_graph, - "import_module", - lambda name: cast(Any, SimpleNamespace(create_deep_agent=fake_create_deep_agent)), + monkeypatch.setitem( + sys.modules, + "deepagents", + cast(Any, SimpleNamespace(create_deep_agent=fake_create_deep_agent)), ) session = AcpSessionContext( @@ -274,13 +470,24 @@ def fake_create_deep_agent(**kwargs: Any) -> object: assert graph is not None assert captured["interrupt_on"] == {"write_file": True} - assert captured["name"] == "deepagents-.deepagents-graph" + assert captured["name"] == "deepagents-ask-.deepagents-graph" + tool_names = {tool.__name__ for tool in cast(list[Any], captured["tools"])} + assert { + "list_workspace_files", + "read_file", + "write_file", + "acp_get_plan", + "acp_set_plan", + "acp_update_plan_entry", + "acp_mark_plan_done", + } <= tool_names def test_deepagents_example_graph_factory_builds_graph_when_dependency_is_mocked( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: + deepagents_graph = _load_example_module(monkeypatch, "examples.langchain.deepagents_graph") fake_graph = object() captured: dict[str, Any] = {} @@ -289,10 +496,10 @@ def fake_create_deep_agent(**kwargs: Any) -> object: return fake_graph monkeypatch.setattr(deepagents_graph, "_deepagents_available", lambda: True) - monkeypatch.setattr( - deepagents_graph, - "import_module", - lambda name: cast(Any, SimpleNamespace(create_deep_agent=fake_create_deep_agent)), + monkeypatch.setitem( + sys.modules, + "deepagents", + cast(Any, SimpleNamespace(create_deep_agent=fake_create_deep_agent)), ) session = AcpSessionContext( @@ -300,17 +507,48 @@ def fake_create_deep_agent(**kwargs: Any) -> object: cwd=tmp_path, created_at=utc_now(), updated_at=utc_now(), + session_model_id="gpt-5.4", + session_mode_id="plan", ) assert deepagents_graph.graph_from_session(session) is fake_graph tool_names = {tool.__name__ for tool in cast(list[Any], captured["tools"])} - assert tool_names == {"list_workspace_files", "read_file", "write_file"} + assert { + "list_workspace_files", + "read_file", + "write_file", + "acp_get_plan", + "acp_set_plan", + "acp_update_plan_entry", + "acp_mark_plan_done", + } <= tool_names + assert captured["name"] == f"deepagents-plan-{tmp_path.name}" + + +def test_deepagents_example_bound_tools_use_session_workspace( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + deepagents_graph = _load_example_module(monkeypatch, "examples.langchain.deepagents_graph") + session_root = tmp_path / "remote-workspace" + session_root.mkdir(parents=True, exist_ok=True) + bound_root = session_root / ".deepagents-graph" + tools = {tool.__name__: tool for tool in deepagents_graph._bind_workspace_tools(bound_root)} + + assert tools["list_workspace_files"]() == "brief.md\nnotes.md" + assert tools["write_file"]("notes.md", "# Deep") == "Mock write accepted for notes.md" + assert tools["list_workspace_files"]() == "brief.md\nnotes.md" + assert "Mock Notes" in tools["read_file"]("notes.md") + + with pytest.raises(ValueError, match="File not found"): + tools["read_file"]("missing.md") def test_deepagents_example_seed_session_and_module_run_as_main( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: + deepagents_graph = _load_example_module(monkeypatch, "examples.langchain.deepagents_graph") root = tmp_path / ".deepagents-graph" monkeypatch.setattr(deepagents_graph, "WORKSPACE_ROOT", root) @@ -326,7 +564,14 @@ def fake_run_acp(*, graph_factory: Any, config: Any) -> None: observed["call"] = (graph_factory, config) monkeypatch.setattr(langchain_acp, "run_acp", fake_run_acp) - runpy.run_module("examples.langchain.deepagents_graph", run_name="__main__") + import codex_auth_helper + + monkeypatch.setattr( + codex_auth_helper, + "create_codex_chat_openai", + lambda _model_name, **_kwargs: _fake_codex_model(), + ) + _run_example_module_as_main("examples.langchain.deepagents_graph") graph_factory, config = observed["call"] assert graph_factory.__name__ == "graph_from_session" diff --git a/tests/langchain/test_low_level_helpers.py b/tests/langchain/test_low_level_helpers.py index cd8d3d3..6864210 100644 --- a/tests/langchain/test_low_level_helpers.py +++ b/tests/langchain/test_low_level_helpers.py @@ -15,6 +15,7 @@ AgentMessageChunk, AgentPlanUpdate, AudioContentBlock, + AvailableCommand, BlobResourceContents, ContentToolCallContent, EmbeddedResourceContentBlock, @@ -28,6 +29,8 @@ SessionConfigOptionBoolean, SessionInfoUpdate, SessionMode, + SessionModelState, + SessionModeState, SseMcpServer, TerminalToolCallContent, TextContentBlock, @@ -40,6 +43,7 @@ ) from langchain_acp import ( AdapterConfig, + ApprovalPolicy, BrowserProjectionMap, BufferedCapabilityBridge, CapabilityBridge, @@ -50,6 +54,7 @@ DeepAgentsCompatibilityBridge, DeepAgentsProjectionMap, DefaultToolClassifier, + ExternalHookEventBridge, FactoryGraphSource, FileSessionStore, FileSystemProjectionMap, @@ -61,6 +66,8 @@ ModelSelectionBridge, ModeSelectionBridge, NativeApprovalBridge, + ProjectionAwareToolClassifier, + SessionMetadataApprovalPolicyStore, StaticGraphSource, StructuredEventProjectionMap, TaskPlan, @@ -77,6 +84,7 @@ create_acp_agent, extract_tool_call_locations, ) +from langchain_acp._slash_commands import validate_mode_command_ids from langchain_acp.approvals import ApprovalDecision from langchain_acp.event_projection import ( _event_payload_to_update, @@ -84,6 +92,7 @@ _normalize_text_content, _resolve_session_update_kind, ) +from langchain_acp.hook_projection import HookEvent from langchain_acp.plan import _bind_native_plan_context from langchain_acp.projection import ( ToolProjection, @@ -111,6 +120,7 @@ _normalized_search_results, _output_text, _parse_structured_value, + _render_path_tree, _search_result_rows, _search_title, _terminal_id, @@ -128,6 +138,19 @@ ) from langchain_acp.runtime.adapter import LangChainAcpAgent from langchain_acp.runtime.server import _resolve_config, _resolve_graph_source, run_acp +from langchain_acp.runtime.slash_commands import ( + McpServerInfo, + ToolInfo, + build_available_commands, + extract_session_mcp_servers, + list_graph_tools, + parse_slash_command, + render_mcp_server_listing, + render_mode_message, + render_model_message, + render_tool_listing, + validate_custom_commands, +) from langchain_acp.serialization import DefaultOutputSerializer, _json_compatible from langchain_acp.session.state import ( AcpSessionContext, @@ -136,7 +159,13 @@ _coerce_json_value, utc_now, ) -from langchain_acp.session.store import _store_lock +from langchain_acp.session.store import _store_child_path, _store_lock +from langchain_acp.slash import ( + SlashCommandRequest, + SlashCommandResult, + StaticSlashCommand, + StaticSlashCommandProvider, +) from langchain_core.messages import AIMessageChunk, ToolMessage from langgraph.types import Command from pydantic import BaseModel @@ -506,6 +535,52 @@ def test_native_approval_bridge_handles_success_cancel_and_invalid_paths() -> No ) ) + +def test_native_approval_bridge_supports_persistent_choices_and_projection_builder() -> None: + bridge = NativeApprovalBridge(enable_persistent_choices=True) + classifier = ProjectionAwareToolClassifier( + base_classifier=DefaultToolClassifier(), + projection_maps=[ + FileSystemProjectionMap(read_tool_names=frozenset({"read_file"})), + ], + ) + client = RecordingACPClient() + client.queue_permission_selected("allow_always") + session = _make_session() + + first_decision = asyncio.run( + bridge.resolve_action_requests( + client=cast(AcpClient, client), + session=session, + action_requests=[{"name": "read_file", "args": {"path": "notes.txt"}}], + review_configs=[{"action_name": "read_file", "allowed_decisions": ["approve"]}], + classifier=classifier, + projection_map=FileSystemProjectionMap(read_tool_names=frozenset({"read_file"})), + ) + ) + second_decision = asyncio.run( + bridge.resolve_action_requests( + client=cast(AcpClient, client), + session=session, + action_requests=[{"name": "read_file", "args": {"path": "notes.txt"}}], + review_configs=[{"action_name": "read_file", "allowed_decisions": ["approve"]}], + classifier=classifier, + projection_map=FileSystemProjectionMap(read_tool_names=frozenset({"read_file"})), + ) + ) + + assert first_decision.decisions == [{"type": "approve"}] + assert second_decision.decisions == [{"type": "approve"}] + assert len(client.permission_requests) == 1 + assert client.permission_requests[0][1] == [ + "allow_once", + "reject_once", + "allow_always", + "reject_always", + ] + assert client.permission_requests[0][2].title == "Read `notes.txt`" + assert client.permission_requests[0][2].kind == "read" + with pytest.raises(RequestError): asyncio.run( bridge.resolve_action_requests( @@ -519,11 +594,12 @@ def test_native_approval_bridge_handles_success_cancel_and_invalid_paths() -> No client = RecordingACPClient() client.queue_permission_selected("unexpected") + unexpected_bridge = NativeApprovalBridge() with pytest.raises(RequestError): asyncio.run( - bridge.resolve_action_requests( + unexpected_bridge.resolve_action_requests( client=cast(AcpClient, client), - session=session, + session=_make_session(), action_requests=[{"name": "read_file", "args": {"path": "notes.txt"}}], review_configs=[], classifier=classifier, @@ -553,6 +629,549 @@ def test_native_approval_bridge_handles_success_cancel_and_invalid_paths() -> No ) +def test_approval_store_and_support_shims_cover_empty_and_reject_paths() -> None: + store = SessionMetadataApprovalPolicyStore() + session = _make_session() + + assert store.export_state(session) is None + assert store.get_policy(session, "missing") is None + + session.metadata[store.metadata_key] = {"bad": "maybe"} + assert store.get_policy(session, "bad") is None + + store.set_policy(session, "dangerous", cast(ApprovalPolicy, "reject")) + assert store.export_state(session) == {"bad": "maybe", "dangerous": "reject"} + + bridge = NativeApprovalBridge(enable_persistent_choices=True) + client = RecordingACPClient() + client.queue_permission_selected("reject_always") + classifier = DefaultToolClassifier() + decision = asyncio.run( + bridge.resolve_action_requests( + client=cast(AcpClient, client), + session=_make_session(session_id="reject-always"), + action_requests=[{"id": "action-1", "name": "terminal", "args": {"command": "pwd"}}], + review_configs=[], + classifier=classifier, + ) + ) + assert decision.decisions == [{"type": "reject"}] + assert client.permission_requests[0][2].tool_call_id == "action-1" + + rejecting_session = _make_session(session_id="reject-remembered") + bridge.policy_store.set_policy(rejecting_session, "terminal", "reject") + remembered = asyncio.run( + bridge.resolve_action_requests( + client=cast(AcpClient, RecordingACPClient()), + session=rejecting_session, + action_requests=[{"name": "terminal", "args": {"command": "pwd"}}], + review_configs=[], + classifier=classifier, + ) + ) + assert remembered.decisions == [{"type": "reject"}] + + non_persistent_bridge = NativeApprovalBridge(enable_persistent_choices=False) + session_without_persist = _make_session(session_id="non-persistent") + non_persistent_bridge._remember_policy(session_without_persist, "terminal", "allow") + assert non_persistent_bridge.policy_store.export_state(session_without_persist) is None + + from langchain_acp.approvals import supports_projection_aware_approval_bridge + + assert supports_projection_aware_approval_bridge(None) is False + assert supports_projection_aware_approval_bridge(NativeApprovalBridge()) is True + assert supports_projection_aware_approval_bridge(cast(Any, object())) is False + + +def test_slash_command_helpers_cover_validation_and_rendering_edges() -> None: + mode_state = SessionModeState( + current_mode_id="ask", + available_modes=[ + SessionMode(id="ask", name="Ask"), + SessionMode(id="review", name="Review"), + ], + ) + model_state = SessionModelState( + current_model_id="openai:gpt-5-mini", + available_models=[ModelInfo(model_id="openai:gpt-5-mini", name="GPT-5 Mini")], + ) + commands = build_available_commands(mode_state=mode_state, model_state=model_state) + assert [command.name for command in commands] == [ + "ask", + "review", + "model", + "tools", + "mcp-servers", + ] + + with pytest.raises(ValueError, match="non-empty"): + validate_mode_command_ids([" "]) + with pytest.raises(ValueError, match="whitespace"): + validate_mode_command_ids(["code review"]) + with pytest.raises(ValueError, match="Duplicate ids: ask"): + validate_mode_command_ids(["ask", " ASK "]) + with pytest.raises(ValueError, match="reserved slash command names"): + validate_mode_command_ids(["model"]) + + with pytest.raises(ValueError, match="already be normalized"): + validate_custom_commands( + [AvailableCommand(name=" Ping ", description="x")], mode_state=None + ) + with pytest.raises(ValueError, match=r"\^\[a-z\]\[a-z0-9-\]\*\$"): + validate_custom_commands([AvailableCommand(name="9ping", description="x")], mode_state=None) + with pytest.raises(ValueError, match="Duplicate ids: ping"): + validate_custom_commands( + [ + AvailableCommand(name="ping", description="x"), + AvailableCommand(name="ping", description="y"), + ], + mode_state=None, + ) + with pytest.raises(ValueError, match="reserved slash command names"): + validate_custom_commands([AvailableCommand(name="tools", description="x")], mode_state=None) + with pytest.raises(ValueError, match="active mode ids"): + validate_custom_commands( + [AvailableCommand(name="ask", description="x")], mode_state=mode_state + ) + validate_custom_commands([AvailableCommand(name="ping", description="x")], mode_state=None) + + parsed = parse_slash_command(" /MODEL openai:gpt-5 ") + assert parsed is not None + assert parsed.name == "model" + assert parsed.argument == "openai:gpt-5" + assert parse_slash_command("hello") is None + assert parse_slash_command("/") is None + assert parse_slash_command("/ ") is None + assert parse_slash_command("/ model") is None + + assert render_mode_message(None) == "Current mode: unavailable" + assert render_model_message(None) == "Current model: unavailable" + assert render_tool_listing([]) == "No tools are currently registered." + assert ( + render_tool_listing([ToolInfo(name="tool", description=None)]) == "Available tools:\n- tool" + ) + assert render_mcp_server_listing([]) == "No MCP servers are currently attached." + assert ( + render_mcp_server_listing( + [McpServerInfo(name="repo", transport="http", target="https://repo", source="session")] + ) + == "MCP servers:\n- repo (http, session): https://repo" + ) + + +def test_slash_command_helpers_cover_graph_tool_and_mcp_server_edge_paths() -> None: + graph = SimpleNamespace( + get_graph=lambda: SimpleNamespace( + nodes={ + "tools": SimpleNamespace( + data=SimpleNamespace( + _tools_by_name={ + "read": SimpleNamespace(description="Read file"), + 1: object(), + } + ) + ), + "other": SimpleNamespace(data=object()), + } + ) + ) + assert list_graph_tools(graph) == [ToolInfo(name="read", description="Read file")] + assert list_graph_tools(SimpleNamespace(get_graph=lambda: SimpleNamespace(nodes=None))) == [] + assert list_graph_tools(SimpleNamespace()) == [] + + session = _make_session() + session.mcp_servers = [ + {"name": "repo-http", "transport": "http", "url": "https://repo.example/mcp"}, + {"name": "repo-http", "transport": "http", "url": "https://repo.example/mcp"}, + {"name": "repo-stdio", "type": "stdio", "command": "python", "args": ["server.py"]}, + {"name": "stdio-no-args", "type": "stdio", "command": "python"}, + {"name": "bad-transport"}, + {"transport": "http"}, + ] + session.metadata["mcp"] = { + "servers": [ + {"name": "bridge", "transport": "sse", "description": "docs"}, + {"name": "bridge", "transport": "sse", "description": "docs"}, + {"name": "bad-bridge", "transport": 1}, + "bad", + cast(Any, {1: "bad"}), + ] + } + infos = extract_session_mcp_servers(session) + assert [(info.name, info.transport, info.source) for info in infos] == [ + ("repo-http", "http", "session"), + ("repo-stdio", "stdio", "session"), + ("stdio-no-args", "stdio", "session"), + ("bridge", "sse", "bridge"), + ] + assert next(info for info in infos if info.name == "stdio-no-args").target == "python" + session.metadata["mcp"] = {"servers": "bad"} # type: ignore[dict-item] + infos = extract_session_mcp_servers(session) + assert len(infos) == 3 + + +def test_static_slash_command_provider_handles_match_and_miss() -> None: + provider = StaticSlashCommandProvider( + commands=[ + StaticSlashCommand( + command=AvailableCommand(name="ping", description="Return pong."), + handler=lambda request: SlashCommandResult(text=request.name), + ) + ] + ) + request = SlashCommandRequest( + name="ping", + argument=None, + raw_prompt="/ping", + session=_make_session(), + graph=object(), + ) + miss_request = SlashCommandRequest( + name="miss", + argument=None, + raw_prompt="/miss", + session=_make_session(), + graph=object(), + ) + assert provider.available_commands(_make_session(), object())[0].name == "ping" + result = provider.handle_command(request) + assert isinstance(result, SlashCommandResult) + assert result.text == "ping" + assert provider.handle_command(miss_request) is None + + +def test_hook_projection_and_external_hook_bridge_cover_hidden_and_start_only_paths() -> None: + hidden_event = HookEvent( + event_id="hidden", + hook_name="before_tool", + summary="hidden summary", + hidden=True, + ) + hidden_bridge = ExternalHookEventBridge() + hidden_session = _make_session(session_id="hidden-hooks") + hidden_bridge.record_event(hidden_session, hidden_event) + assert hidden_bridge.drain_updates(hidden_session) is None + + projection_map = cast(Any, ExternalHookEventBridge()).projection_map + assert projection_map.build_start_update(tool_call_id="x", event=hidden_event) is None + assert projection_map.build_progress_update(tool_call_id="x", event=hidden_event) is None + + class _StartOnlyProjectionMap: + hidden_event_ids: set[str] = set() + title_prefix = "Hook" + + def build_start_update(self, *, tool_call_id: str, event: HookEvent) -> ToolCallStart: + return ToolCallStart( + session_update="tool_call", + tool_call_id=tool_call_id, + title=event.hook_name, + kind="execute", + status="in_progress", + ) + + def build_progress_update(self, *, tool_call_id: str, event: HookEvent) -> None: + del tool_call_id, event + return None + + bridge = ExternalHookEventBridge( + emission_mode="start_only", + projection_map=cast(Any, _StartOnlyProjectionMap()), + ) + session = _make_session(session_id="external-hooks") + bridge.record_event( + session, + HookEvent( + event_id="start-only", + hook_name="after_tool", + summary="done", + status="completed", + ), + ) + updates = bridge.drain_updates(session) + assert updates is not None + assert len(updates) == 1 + assert getattr(updates[0], "status", None) == "completed" + bridge.record_event( + session, + HookEvent(event_id="start-only-no-status", hook_name="after_tool", summary="pending"), + ) + no_status_updates = bridge.drain_updates(session) + assert no_status_updates is not None + assert len(no_status_updates) == 1 + + fallback_bridge = ExternalHookEventBridge( + projection_map=cast(Any, _StartOnlyProjectionMap()), + ) + fallback_bridge.record_event( + session, + HookEvent(event_id="paired-no-progress", hook_name="after_tool", summary="done"), + ) + fallback_updates = fallback_bridge.drain_updates(session) + assert fallback_updates is not None + assert len(fallback_updates) == 1 + + +def test_adapter_slash_helpers_cover_unhandled_and_surface_refresh_paths() -> None: + @dataclass(slots=True) + class _NullGraphSource: + graph: Any = field(default_factory=object) + + async def get_graph(self, session: AcpSessionContext) -> Any: + del session + return self.graph + + @dataclass(slots=True) + class _EmptyClient: + updates: list[Any] = field(default_factory=list) + + async def session_update(self, session_id: str, update: Any, **kwargs: Any) -> None: + del session_id, kwargs + self.updates.append(update) + + class _CustomProvider: + def __init__(self, result: SlashCommandResult | None) -> None: + self.result = result + + def available_commands( + self, session: AcpSessionContext, graph: Any + ) -> list[AvailableCommand]: + del session, graph + return [AvailableCommand(name="ping", description="pong")] + + def handle_command(self, request: SlashCommandRequest) -> SlashCommandResult | None: + del request + return self.result + + adapter = LangChainAcpAgent( + cast(Any, _NullGraphSource()), + config=AdapterConfig(), + ) + client = _EmptyClient() + adapter.on_connect(cast(AcpClient, client)) + session = _make_session() + null_graph_source = _NullGraphSource() + assert asyncio.run(null_graph_source.get_graph(session)) is null_graph_source.graph + custom_provider = _CustomProvider(None) + assert [command.name for command in custom_provider.available_commands(session, object())] == [ + "ping" + ] + assert ( + asyncio.run( + adapter._maybe_handle_slash_prompt( + session=session, + graph=object(), + prompt=[], + acknowledged_message_id=None, + ) + ) + is None + ) + assert ( + asyncio.run( + adapter._maybe_handle_slash_prompt( + session=session, + graph=object(), + prompt=[cast(Any, object())], + acknowledged_message_id=None, + ) + ) + is None + ) + assert ( + asyncio.run( + adapter._maybe_handle_slash_prompt( + session=session, + graph=object(), + prompt=[TextContentBlock(type="text", text="/unknown")], + acknowledged_message_id=None, + ) + ) + is None + ) + + provider_adapter = LangChainAcpAgent( + cast(Any, _NullGraphSource()), + config=AdapterConfig(slash_command_provider=cast(Any, custom_provider)), + ) + provider_adapter.on_connect(cast(AcpClient, client)) + assert ( + asyncio.run( + provider_adapter._maybe_handle_slash_prompt( + session=session, + graph=object(), + prompt=[TextContentBlock(type="text", text="/ping")], + acknowledged_message_id="msg-1", + ) + ) + is None + ) + + handled_false_adapter = LangChainAcpAgent( + cast(Any, _NullGraphSource()), + config=AdapterConfig( + slash_command_provider=cast( + Any, _CustomProvider(SlashCommandResult(handled=False, text="ignored")) + ) + ), + ) + handled_false_adapter.on_connect(cast(AcpClient, client)) + assert ( + asyncio.run( + handled_false_adapter._maybe_handle_slash_prompt( + session=session, + graph=object(), + prompt=[TextContentBlock(type="text", text="/ping")], + acknowledged_message_id="msg-2", + ) + ) + is None + ) + + no_text_result = SlashCommandResult( + text=None, + updates=[ + AgentMessageChunk( + session_update="agent_message_chunk", + content=TextContentBlock(type="text", text="update-only"), + ) + ], + refresh_session_surface=False, + ) + no_text_adapter = LangChainAcpAgent( + cast(Any, _NullGraphSource()), + config=AdapterConfig(slash_command_provider=cast(Any, _CustomProvider(no_text_result))), + ) + no_text_adapter.on_connect(cast(AcpClient, client)) + response = asyncio.run( + no_text_adapter._maybe_handle_slash_prompt( + session=session, + graph=object(), + prompt=[TextContentBlock(type="text", text="/ping")], + acknowledged_message_id="msg-3", + ) + ) + assert response is not None + assert response.stop_reason == "end_turn" + + class _ModeBridge(CapabilityBridge): + def get_mode_state(self, session: AcpSessionContext) -> ModeState: + del session + return ModeState( + modes=[SessionMode(id="review", name="Review")], + current_mode_id="review", + enable_config_option=False, + ) + + def set_mode(self, session: AcpSessionContext, mode_id: str) -> ModeState | None: + del session + if mode_id == "review": + return None + return ModeState(modes=[], current_mode_id=None, enable_config_option=False) + + unavailable_mode_state = _ModeBridge().set_mode(session, "other") + assert unavailable_mode_state is not None + assert unavailable_mode_state.current_mode_id is None + + mode_adapter = LangChainAcpAgent( + cast(Any, _NullGraphSource()), + config=AdapterConfig(capability_bridges=[_ModeBridge()]), + ) + mode_adapter.on_connect(cast(AcpClient, client)) + assert ( + asyncio.run( + mode_adapter._handle_builtin_slash_command( + session=session, + graph=object(), + command_name="review", + argument=None, + ) + ) + == "Mode is unavailable or invalid" + ) + + class _NoneModeBridge(_ModeBridge): + def set_mode(self, session: AcpSessionContext, mode_id: str) -> ModeState | None: + del session, mode_id + return ModeState( + modes=[SessionMode(id="review", name="Review")], + current_mode_id=None, + enable_config_option=False, + ) + + none_mode_state = _NoneModeBridge().set_mode(session, "review") + assert none_mode_state is not None + assert none_mode_state.current_mode_id is None + + none_mode_adapter = LangChainAcpAgent( + cast(Any, _NullGraphSource()), + config=AdapterConfig(capability_bridges=[_NoneModeBridge()]), + ) + none_mode_adapter.on_connect(cast(AcpClient, client)) + + async def _return_none_mode( + _session: AcpSessionContext, + _mode_id: str, + ) -> ModeState | None: + return ModeState( + modes=[SessionMode(id="review", name="Review")], + current_mode_id=None, + enable_config_option=False, + ) + + cast(Any, none_mode_adapter)._set_mode = _return_none_mode + assert ( + asyncio.run( + none_mode_adapter._handle_builtin_slash_command( + session=session, + graph=object(), + command_name="review", + argument=None, + ) + ) + == "Mode is unavailable or invalid" + ) + + class _ModelBridge(CapabilityBridge): + def get_model_state(self, session: AcpSessionContext) -> ModelSelectionState: + del session + return ModelSelectionState( + current_model_id=None, + available_models=[ModelInfo(model_id="base", name="Base")], + enable_config_option=False, + ) + + model_state = _ModelBridge().get_model_state(session) + assert model_state.current_model_id is None + + model_adapter = LangChainAcpAgent( + cast(Any, _NullGraphSource()), + config=AdapterConfig(capability_bridges=[_ModelBridge()]), + ) + model_adapter.on_connect(cast(AcpClient, client)) + assert ( + asyncio.run( + model_adapter._handle_builtin_slash_command( + session=session, + graph=object(), + command_name="model", + argument=None, + ) + ) + == "Current model: unavailable" + ) + assert ( + asyncio.run( + model_adapter._handle_builtin_slash_command( + session=session, + graph=object(), + command_name="model", + argument="bad-model", + ) + ) + == "Model is unavailable or invalid" + ) + + def test_phase5_builtin_bridges_cover_direct_paths(tmp_path: Path) -> None: session = _make_session(cwd=tmp_path) session.config_values["plan_generation_type"] = "tools" @@ -905,11 +1524,45 @@ def test_projection_helpers_cover_classification_composition_and_locations() -> assert classifier.classify("terminal") == "execute" assert classifier.classify("unknown_tool") == "other" assert classifier.approval_policy_key("read_file") == "read_file" + projection_classifier = ProjectionAwareToolClassifier( + base_classifier=classifier, + projection_maps=[ + FileSystemProjectionMap(search_tool_names=frozenset({"glob"})), + ], + ) + assert projection_classifier.classify("glob", {"path": "."}) == "search" + assert projection_classifier.classify("unknown_tool", {"path": "."}) == "other" + nested_classifier = ProjectionAwareToolClassifier( + base_classifier=classifier, + projection_maps=[ + CompositeProjectionMap( + maps=( + FileSystemProjectionMap( + write_tool_names=frozenset({"write_file"}), + execute_tool_names=frozenset({"execute_shell"}), + ), + ) + ) + ], + ) + assert nested_classifier.classify("write_file") == "edit" + assert nested_classifier.classify("execute_shell") == "execute" + fallback_nested_classifier = ProjectionAwareToolClassifier( + base_classifier=classifier, + projection_maps=[ + CompositeProjectionMap(maps=(_StaticProjectionMap(),)), + FileSystemProjectionMap(search_tool_names=frozenset({"grep"})), + ], + ) + assert fallback_nested_classifier.classify("grep") == "search" projection_map = FileSystemProjectionMap( write_tool_names=frozenset({"write_file"}), read_tool_names=frozenset({"read_file"}), + search_tool_names=frozenset({"glob"}), execute_tool_names=frozenset({"execute_shell"}), + render_search_results_as_tree=True, + hide_dot_directories_in_tree=True, ) execute_start = projection_map.project_start( @@ -949,6 +1602,11 @@ def test_projection_helpers_cover_classification_composition_and_locations() -> ) assert search_start is not None assert search_start.title == "Glob `*.py`" + search_projection_with_default = FileSystemProjectionMap( + search_tool_names=frozenset({"glob"}), + default_search_tool="file_search", + ) + assert "file_search" in search_projection_with_default._search_tool_names() assert projection_map.project_start("other_tool", raw_input={}) is None assert projection_map.project_start("read_file", raw_input="bad") is None assert projection_map.project_start("execute_shell", raw_input={"path": "notes.txt"}) is None @@ -967,6 +1625,50 @@ def test_projection_helpers_cover_classification_composition_and_locations() -> ) assert read_progress is not None assert read_progress.locations == [ToolCallLocation(path="notes.txt")] + search_progress = projection_map.project_progress( + "glob", + raw_input={"path": ".", "pattern": "*.py"}, + raw_output="./src/main.py\n./.venv/ignored.py\n./tests/test_app.py\n...", + serialized_output="./src/main.py\n./.venv/ignored.py\n./tests/test_app.py\n...", + status="completed", + ) + assert search_progress is not None + assert search_progress.content is not None + assert isinstance(search_progress.content[0], ContentToolCallContent) + assert "Tree: ." in search_progress.content[0].content.text + assert ".venv" not in search_progress.content[0].content.text + assert _render_path_tree("\n", root_label=".", hide_dot_directories=False) == "\n" + assert _render_path_tree("./", root_label=".", hide_dot_directories=False) == "./" + assert _render_path_tree("...\n", root_label=".", hide_dot_directories=False) == "...\n" + assert "Tree: ." in _render_path_tree( + "src/\nsrc/main.py\n", + root_label=".", + hide_dot_directories=False, + ) + assert _render_path_tree( + ".hidden/file.py\n", root_label=".", hide_dot_directories=False + ).startswith("Tree: .") + assert _render_path_tree("/", root_label=".", hide_dot_directories=False).startswith("Tree: .") + flat_search_progress = search_projection_with_default.project_progress( + "file_search", + raw_input={"path": "src", "pattern": "*.py"}, + raw_output="a.py\nb.py", + serialized_output="a.py\nb.py", + status="completed", + ) + assert flat_search_progress is not None + assert flat_search_progress.content is not None + assert flat_search_progress.content[0].content.text == "a.py\nb.py" + assert ( + search_projection_with_default.project_progress( + "file_search", + raw_input="bad", + raw_output="ignored", + serialized_output="ignored", + status="completed", + ) + is None + ) assert ( projection_map.project_progress( "read_file", @@ -1112,6 +1814,37 @@ def test_projection_helpers_cover_classification_composition_and_locations() -> ) assert deepagents_progress is not None assert deepagents_progress.content is None + assert ( + deepagents_map.project_progress( + "glob", + raw_input={"path": "", "pattern": "*.py"}, + raw_output="", + serialized_output="", + status="completed", + ) + is not None + ) + + +def test_external_hook_event_bridge_emits_buffered_updates_and_metadata() -> None: + bridge = ExternalHookEventBridge() + session = _make_session() + bridge.record_event( + session, + HookEvent( + event_id="before-tool", + hook_name="before_tool", + summary="Before tool hook fired.", + detail="Before tool hook completed.", + ), + ) + + metadata = bridge.get_session_metadata(session) + updates = bridge.drain_updates(session) + + assert metadata["emission_mode"] == "paired" + assert updates is not None + assert len(updates) == 2 def test_web_projection_maps_render_search_and_fetch_results() -> None: @@ -2093,8 +2826,24 @@ def test_memory_and_file_session_stores_cover_lifecycle(tmp_path: Path) -> None: stale_path.write_text("stale", encoding="utf-8") file_store = FileSessionStore(tmp_path) assert not stale_path.exists() + assert file_store._session_path("safe_1-2").parent == file_store.root + assert file_store._temp_session_path("safe_1-2").parent == file_store.root + with pytest.raises(ValueError, match="session path"): + _store_child_path(file_store.root, "../escape.json") + + unsafe_session_ids = ["", "../escape", "nested/session", ".hidden", "two words", "x" * 129] + for unsafe_session_id in unsafe_session_ids: + with pytest.raises(ValueError, match="session_id"): + file_store._session_path(unsafe_session_id) + + with pytest.raises(ValueError, match="session_id"): + file_store.save(_make_session(session_id="../escape", cwd=tmp_path)) + with pytest.raises(ValueError, match="session_id"): + file_store.get("../escape") file_store.save(session) + with pytest.raises(ValueError, match="session_id"): + file_store.fork("session-1", new_session_id="../escape", cwd=tmp_path) file_loaded = file_store.get(session.session_id) assert file_loaded is not None assert file_loaded.transcript[0].to_update().message_id == "u1" @@ -2146,6 +2895,8 @@ def test_memory_and_file_session_stores_cover_lifecycle(tmp_path: Path) -> None: invalid_session_path = tmp_path / "invalid.json" invalid_session_path.write_text("[]", encoding="utf-8") + invalid_name_path = tmp_path / "bad name.json" + invalid_name_path.write_text("{}", encoding="utf-8") assert all(item.session_id != "invalid" for item in file_store.list_sessions()) diff --git a/tests/langchain/test_runtime.py b/tests/langchain/test_runtime.py index 9d2a90a..665b87e 100644 --- a/tests/langchain/test_runtime.py +++ b/tests/langchain/test_runtime.py @@ -10,8 +10,12 @@ from acp.schema import ( AgentPlanUpdate, AudioContentBlock, + AvailableCommand, + AvailableCommandsUpdate, BlobResourceContents, + ConfigOptionUpdate, ContentToolCallContent, + CurrentModeUpdate, EmbeddedResourceContentBlock, ModelInfo, PlanEntry, @@ -26,6 +30,7 @@ from langchain.agents.middleware import HumanInTheLoopMiddleware from langchain_acp import ( AdapterConfig, + AdapterPromptCapabilities, BrowserProjectionMap, CapabilityBridge, CommandProjectionMap, @@ -34,12 +39,15 @@ FileSystemProjectionMap, FinanceProjectionMap, HttpRequestProjectionMap, + StaticSlashCommand, + StaticSlashCommandProvider, StructuredEventProjectionMap, WebSearchProjectionMap, create_acp_agent, native_plan_tools, ) from langchain_acp.providers import ModelSelectionState, ModeState +from langchain_acp.slash import SlashCommandResult from langchain_core.messages import AIMessage from langgraph.graph import START, StateGraph @@ -109,6 +117,110 @@ def read_file(path: str) -> str: assert agent_message_texts(client) == ["Done reading."] +def test_langchain_acp_initialize_uses_configured_prompt_capabilities() -> None: + graph = create_agent( + model=GenericFakeChatModel(messages=iter([])), + tools=[], + name="caps", + ) + adapter = create_acp_agent( + graph=graph, + config=AdapterConfig( + prompt_capabilities=AdapterPromptCapabilities( + audio=False, + image=False, + embedded_context=True, + ) + ), + ) + + response = asyncio.run(adapter.initialize(protocol_version=1)) + assert response.agent_capabilities is not None + assert response.agent_capabilities.prompt_capabilities is not None + + assert response.agent_capabilities.prompt_capabilities.audio is False + assert response.agent_capabilities.prompt_capabilities.image is False + assert response.agent_capabilities.prompt_capabilities.embedded_context is True + + +def test_langchain_acp_slash_commands_emit_surface_updates_and_skip_graph(tmp_path) -> None: + def read_file(path: str) -> str: + """Read a file from the workspace.""" + return path + + assert read_file("repo.txt") == "repo.txt" + + graph = create_agent( + model=GenericFakeChatModel(messages=iter([])), + tools=[read_file], + name="slash-reader", + ) + adapter = create_acp_agent( + graph=graph, + config=AdapterConfig( + available_models=[ + ModelInfo(model_id="openai:gpt-5-mini", name="GPT-5 Mini"), + ModelInfo(model_id="openai:gpt-5", name="GPT-5"), + ], + available_modes=[ + SessionMode(id="ask", name="Ask"), + SessionMode(id="review", name="Review"), + ], + default_model_id="openai:gpt-5-mini", + default_mode_id="ask", + slash_command_provider=StaticSlashCommandProvider( + commands=[ + StaticSlashCommand( + command=AvailableCommand(name="ping", description="Return pong."), + handler=lambda _request: SlashCommandResult(text="pong"), + ) + ] + ), + ), + ) + client = RecordingACPClient() + adapter.on_connect(cast(AcpClient, client)) + + session = asyncio.run(adapter.new_session(cwd=str(tmp_path), mcp_servers=[])) + stored_session = cast(Any, adapter)._store.get(session.session_id) + assert stored_session is not None + stored_session.mcp_servers = [ + {"name": "repo-http", "transport": "http", "url": "https://repo.example/mcp"} + ] + cast(Any, adapter)._store.save(stored_session) + client.updates.clear() + + asyncio.run(adapter.prompt(prompt=[text_block("/tools")], session_id=session.session_id)) + asyncio.run(adapter.prompt(prompt=[text_block("/review")], session_id=session.session_id)) + asyncio.run( + adapter.prompt(prompt=[text_block("/model openai:gpt-5")], session_id=session.session_id) + ) + asyncio.run(adapter.prompt(prompt=[text_block("/mcp-servers")], session_id=session.session_id)) + asyncio.run(adapter.prompt(prompt=[text_block("/ping")], session_id=session.session_id)) + + assert agent_message_texts(client) == [ + "Available tools:\n- read_file: Read a file from the workspace.", + "Current mode: review", + "Current model: openai:gpt-5", + "MCP servers:\n- repo-http (http, session): https://repo.example/mcp", + "pong", + ] + assert any(isinstance(update, CurrentModeUpdate) for _, update in client.updates) + assert any(isinstance(update, ConfigOptionUpdate) for _, update in client.updates) + available_command_updates = [ + update for _, update in client.updates if isinstance(update, AvailableCommandsUpdate) + ] + assert available_command_updates + assert [command.name for command in available_command_updates[0].available_commands] == [ + "ask", + "review", + "model", + "tools", + "mcp-servers", + "ping", + ] + + def test_langchain_acp_projects_web_search_results_at_runtime(tmp_path) -> None: def duckduckgo_results_json(query: str) -> list[dict[str, str]]: """Search the web and return result records.""" @@ -678,7 +790,7 @@ def graph_factory(session) -> Any: asyncio.run(adapter.prompt(prompt=[text_block("hello")], session_id=session.session_id)) - assert captured_session_ids == [session.session_id] + assert captured_session_ids == [session.session_id, session.session_id] assert agent_message_texts(client) == ["Factory graph ready."] @@ -768,7 +880,7 @@ def graph_factory(session) -> Any: ) assert second.stop_reason == "end_turn" assert agent_message_texts(client)[-1] == "gpt-5:plan" - assert captured_builds == [("base", "ask"), ("gpt-5", "plan")] + assert captured_builds == [("base", "ask"), ("base", "ask"), ("gpt-5", "plan")] @dataclass(slots=True, kw_only=True) diff --git a/tests/pydantic/support.py b/tests/pydantic/support.py index 9a51a82..cda330c 100644 --- a/tests/pydantic/support.py +++ b/tests/pydantic/support.py @@ -44,22 +44,28 @@ from pydantic_acp import ( AcpSessionContext, AdapterConfig, + AdapterPromptCapabilities, AgentBridgeBuilder, AgentFactory, AgentSource, AnthropicCompactionBridge, + ApprovalPolicy, + ApprovalPolicyStore, BuiltinToolProjectionMap, ClientFilesystemBackend, ClientHostContext, ClientTerminalBackend, CompositeProjectionMap, ConfigOption, + DefaultPermissionToolCallBuilder, + ExternalHookEventBridge, FactoryAgentSource, FileSessionStore, FilesystemBackend, FileSystemProjectionMap, HistoryProcessorBridge, HookBridge, + HookEvent, HookProjectionMap, ImageGenerationBridge, IncludeToolReturnSchemasBridge, @@ -72,11 +78,23 @@ ModeState, NativeApprovalBridge, OpenAICompactionBridge, + PermissionOptionSet, + PermissionRequestContext, + PermissionToolCallBuilder, PrefixToolsBridge, + PrepareOutputToolsBridge, + PrepareOutputToolsMode, PrepareToolsBridge, PrepareToolsMode, + ProjectionAwareToolClassifier, + SessionMetadataApprovalPolicyStore, SetToolMetadataBridge, + SlashCommandProvider, + SlashCommandRequest, + SlashCommandResult, StaticAgentSource, + StaticSlashCommand, + StaticSlashCommandProvider, TerminalBackend, ThinkingBridge, ThreadExecutorBridge, @@ -98,6 +116,7 @@ "AcpSessionContext", "AdapterConfig", "AdapterModel", + "AdapterPromptCapabilities", "Agent", "AgentBridgeBuilder", "AgentFactory", @@ -105,6 +124,8 @@ "AgentPlanUpdate", "AgentSource", "AllowedOutcome", + "ApprovalPolicy", + "ApprovalPolicyStore", "ApprovalRequired", "AsyncDemoConfigOptionsProvider", "AsyncDemoModesProvider", @@ -121,6 +142,7 @@ "ConfigOptionUpdate", "CreateTerminalResponse", "CurrentModeUpdate", + "DefaultPermissionToolCallBuilder", "DemoApprovalStateProvider", "DemoConfigOptionsProvider", "DemoModelsProvider", @@ -128,6 +150,7 @@ "DemoPlanProvider", "DeniedOutcome", "EnvVariable", + "ExternalHookEventBridge", "FactoryAgentSource", "FileSessionStore", "FileEditToolCallContent", @@ -137,6 +160,7 @@ "FreeformModelsProvider", "HistoryProcessorBridge", "HookBridge", + "HookEvent", "HookProjectionMap", "ImageGenerationBridge", "IncludeToolReturnSchemasBridge", @@ -154,9 +178,15 @@ "NativeApprovalBridge", "OpenAICompactionBridge", "Path", + "PermissionOptionSet", "PermissionOption", + "PermissionRequestContext", + "PermissionToolCallBuilder", "PlanEntry", "PrefixToolsBridge", + "ProjectionAwareToolClassifier", + "PrepareOutputToolsBridge", + "PrepareOutputToolsMode", "PrepareToolsBridge", "PrepareToolsMode", "ReadTextFileResponse", @@ -170,9 +200,15 @@ "SessionConfigSelectGroup", "SessionConfigSelectOption", "SessionInfoUpdate", + "SessionMetadataApprovalPolicyStore", "SessionMode", "SetToolMetadataBridge", + "SlashCommandProvider", + "SlashCommandRequest", + "SlashCommandResult", "StaticAgentSource", + "StaticSlashCommand", + "StaticSlashCommandProvider", "TerminalToolCallContent", "TerminalBackend", "TerminalOutputResponse", @@ -204,6 +240,7 @@ class RecordingClient: def __init__(self) -> None: self.updates: list[tuple[str, Any]] = [] self.permission_option_ids: list[tuple[str, list[str], ToolCallUpdate]] = [] + self.permission_option_names: list[tuple[str, list[str], ToolCallUpdate]] = [] self.permission_responses: list[RequestPermissionResponse] = [] def queue_permission_selected(self, option_id: str) -> None: @@ -229,6 +266,9 @@ async def request_permission( self.permission_option_ids.append( (session_id, [option.option_id for option in options], tool_call) ) + self.permission_option_names.append( + (session_id, [option.name for option in options], tool_call) + ) if not self.permission_responses: raise AssertionError("unexpected permission request") return self.permission_responses.pop(0) diff --git a/tests/pydantic/test_adapter_helpers.py b/tests/pydantic/test_adapter_helpers.py index f6cf94d..d6a7376 100644 --- a/tests/pydantic/test_adapter_helpers.py +++ b/tests/pydantic/test_adapter_helpers.py @@ -1063,6 +1063,7 @@ async def failing_execute(**kwargs: Any) -> Any: ) assert adapter._build_run_kwargs( + session=session, message_history=None, deferred_tool_results=None, deps=None, @@ -1071,7 +1072,9 @@ async def failing_execute(**kwargs: Any) -> Any: output_type=None, ) == { "deferred_tool_results": None, + "conversation_id": session.session_id, "message_history": None, + "metadata": {"pydantic_acp_session_id": session.session_id}, "model": None, } adapter._config.approval_bridge = None diff --git a/tests/pydantic/test_approvals.py b/tests/pydantic/test_approvals.py index 0f0027e..0003f32 100644 --- a/tests/pydantic/test_approvals.py +++ b/tests/pydantic/test_approvals.py @@ -3,32 +3,61 @@ import asyncio from typing import Any, cast +import pydantic_acp.approvals as approvals_module import pytest +from pydantic_acp import supports_projection_aware_approval_bridge +from pydantic_acp.approvals import ApprovalResolution from pydantic_acp.runtime.prompts import dump_message_history, load_message_history from pydantic_ai import ModelRequest, ModelResponse, TextPart, ToolCallPart from pydantic_ai.messages import UserPromptPart from pydantic_ai.models.function import AgentInfo, FunctionModel +from pydantic_ai.tools import DeferredToolRequests, DeferredToolResults, ToolApproved from .support import ( + UTC, + AcpSessionContext, AdapterConfig, Agent, + ApprovalPolicy, ApprovalRequired, AvailableCommandsUpdate, FileEditToolCallContent, FileSystemProjectionMap, + JsonValue, MemorySessionStore, + NativeApprovalBridge, Path, + PermissionOptionSet, + PermissionRequestContext, RecordingClient, RunContext, + SessionMetadataApprovalPolicyStore, TestModel, ToolCallProgress, ToolCallStart, + ToolCallUpdate, agent_message_texts, create_acp_agent, + datetime, text_block, ) +class _RecordingPermissionBuilder: + def __init__(self) -> None: + self.contexts: list[PermissionRequestContext] = [] + + def build_tool_call_update(self, context: PermissionRequestContext) -> ToolCallUpdate: + self.contexts.append(context) + return ToolCallUpdate( + tool_call_id=context.tool_call.tool_call_id, + title="Custom Permission", + kind="edit", + status="pending", + raw_input=context.raw_input, + ) + + def test_deferred_approval_allow_flow_resumes_run(tmp_path: Path) -> None: agent = Agent(TestModel(call_tools=["dangerous"])) @@ -302,6 +331,301 @@ def write_file(ctx: RunContext[None], path: str, content: str) -> str: assert tool_progress.status == "completed" +def test_deferred_approval_permission_request_uses_projection_content( + tmp_path: Path, +) -> None: + agent = Agent(TestModel(call_tools=["write_file"])) + + @agent.tool + def write_file(ctx: RunContext[None], path: str, content: str) -> str: + if not ctx.tool_call_approved: + raise ApprovalRequired() + return f"approved:{path}" # pragma: no cover + + adapter = create_acp_agent( + agent=agent, + config=AdapterConfig(session_store=MemorySessionStore()), + projection_maps=[FileSystemProjectionMap(default_write_tool="write_file")], + ) + client = RecordingClient() + client.queue_permission_selected("allow_once") + adapter.on_connect(client) + + session = asyncio.run(adapter.new_session(cwd=str(tmp_path), mcp_servers=[])) + asyncio.run( + adapter.prompt( + prompt=[text_block("Use the write tool.")], + session_id=session.session_id, + ) + ) + + permission_request = client.permission_option_ids[0][2] + assert permission_request.status == "pending" + assert permission_request.content is not None + content = permission_request.content[0] + assert isinstance(content, FileEditToolCallContent) + assert content.path == "a" + assert content.new_text == "a" + + +def test_native_approval_bridge_uses_custom_builder_store_and_labels(tmp_path: Path) -> None: + agent = Agent(TestModel(call_tools=["write_file"])) + + @agent.tool + def write_file(ctx: RunContext[None], path: str, content: str) -> str: + if not ctx.tool_call_approved: + raise ApprovalRequired() + return f"approved:{path}" # pragma: no cover + + class Store: + def __init__(self) -> None: + self.policies: dict[str, ApprovalPolicy] = {} + self.get_calls: list[str] = [] + self.set_calls: list[tuple[str, ApprovalPolicy]] = [] + + def get_policy( + self, + session: AcpSessionContext, + policy_key: str, + ) -> ApprovalPolicy | None: + del session + self.get_calls.append(policy_key) + return self.policies.get(policy_key) + + def set_policy( + self, + session: AcpSessionContext, + policy_key: str, + policy: ApprovalPolicy, + ) -> None: + del session + self.set_calls.append((policy_key, policy)) + self.policies[policy_key] = policy + + def export_state(self, session: AcpSessionContext) -> dict[str, JsonValue]: + del session + return dict(self.policies) + + store = Store() + builder = _RecordingPermissionBuilder() + adapter = create_acp_agent( + agent=agent, + config=AdapterConfig( + approval_bridge=NativeApprovalBridge( + enable_persistent_choices=True, + option_set=PermissionOptionSet( + allow_once_name="Allow this", + reject_once_name="Deny this", + allow_always_name="Always yes", + reject_always_name="Always no", + ), + policy_store=store, + tool_call_builder=builder, + ), + session_store=MemorySessionStore(), + ), + ) + client = RecordingClient() + client.queue_permission_selected("allow_always") + adapter.on_connect(client) + + session = asyncio.run(adapter.new_session(cwd=str(tmp_path), mcp_servers=[])) + asyncio.run( + adapter.prompt( + prompt=[text_block("Use the write tool.")], + session_id=session.session_id, + ) + ) + + assert client.permission_option_ids[0][1] == [ + "allow_once", + "allow_always", + "reject_once", + "reject_always", + ] + assert client.permission_option_names[0][1] == [ + "Allow this", + "Always yes", + "Deny this", + "Always no", + ] + assert client.permission_option_ids[0][2].title == "Custom Permission" + assert len(builder.contexts) == 1 + assert store.get_calls == ["write_file"] + assert store.set_calls == [("write_file", "allow")] + export_session = AcpSessionContext( + session_id=session.session_id, + cwd=tmp_path, + created_at=datetime.now(UTC), + updated_at=datetime.now(UTC), + ) + assert store.export_state(export_session) == {"write_file": "allow"} + + +def test_native_approval_bridge_live_policy_lookup_does_not_export_state( + tmp_path: Path, +) -> None: + agent = Agent(TestModel(call_tools=["dangerous"])) + + @agent.tool + def dangerous(ctx: RunContext[None], path: str) -> str: + if not ctx.tool_call_approved: + raise ApprovalRequired() + return f"approved:{path}" # pragma: no cover + + class Store: + def __init__(self) -> None: + self.get_calls: list[str] = [] + + def get_policy( + self, + session: AcpSessionContext, + policy_key: str, + ) -> ApprovalPolicy | None: + del session + self.get_calls.append(policy_key) + return "allow" + + def set_policy( + self, + session: AcpSessionContext, + policy_key: str, + policy: ApprovalPolicy, + ) -> None: # pragma: no cover + del session, policy_key, policy + raise AssertionError("remembered policy should not be rewritten") + + def export_state( + self, session: AcpSessionContext + ) -> dict[str, JsonValue]: # pragma: no cover + del session + raise AssertionError("export_state is metadata-only, not live approval lookup") + + store = Store() + adapter = create_acp_agent( + agent=agent, + config=AdapterConfig( + approval_bridge=NativeApprovalBridge( + enable_persistent_choices=True, + policy_store=store, + ), + session_store=MemorySessionStore(), + ), + ) + client = RecordingClient() + adapter.on_connect(client) + + session = asyncio.run(adapter.new_session(cwd=str(tmp_path), mcp_servers=[])) + response = asyncio.run( + adapter.prompt( + prompt=[text_block("Use the dangerous tool.")], + session_id=session.session_id, + ) + ) + + assert response.stop_reason == "end_turn" + assert store.get_calls == ["dangerous"] + assert client.permission_option_ids == [] + assert agent_message_texts(client) == ['{"dangerous":"approved:a"}'] + + +def test_session_metadata_approval_policy_store_reads_valid_policy(tmp_path: Path) -> None: + session = AcpSessionContext( + session_id="approval-store", + cwd=tmp_path, + created_at=datetime.now(UTC), + updated_at=datetime.now(UTC), + metadata={"approval_policies": {"write_file": "allow"}}, + ) + store = SessionMetadataApprovalPolicyStore() + + assert store.get_policy(session, "write_file") == "allow" + + +def test_projection_aware_approval_bridge_detection_edges( + monkeypatch: pytest.MonkeyPatch, +) -> None: + class MissingCallable: + resolve_deferred_approvals = None + + class VarKeywordBridge: + async def resolve_deferred_approvals(self, **kwargs: Any) -> ApprovalResolution: + raise AssertionError("not called") + + class SignatureBridge: + async def resolve_deferred_approvals(self) -> ApprovalResolution: + raise AssertionError("not called") + + assert not supports_projection_aware_approval_bridge(MissingCallable()) + assert supports_projection_aware_approval_bridge(VarKeywordBridge()) + with pytest.raises(AssertionError, match="not called"): + asyncio.run(VarKeywordBridge().resolve_deferred_approvals()) + + def raise_signature_error(value: object) -> object: + del value + raise ValueError("signature unavailable") + + monkeypatch.setattr(approvals_module, "signature", raise_signature_error) + assert not supports_projection_aware_approval_bridge(SignatureBridge()) + with pytest.raises(AssertionError, match="not called"): + asyncio.run(SignatureBridge().resolve_deferred_approvals()) + + +def test_legacy_approval_bridge_without_projection_signature_still_runs( + tmp_path: Path, +) -> None: + class LegacyBridge: + def __init__(self) -> None: + self.called = False + + async def resolve_deferred_approvals( + self, + *, + client: Any, + session: AcpSessionContext, + requests: DeferredToolRequests, + classifier: Any, + ) -> ApprovalResolution: + del client, session, classifier + self.called = True + results = DeferredToolResults(metadata=dict(requests.metadata)) + for tool_call in requests.approvals: + results.approvals[tool_call.tool_call_id] = ToolApproved() + return ApprovalResolution(deferred_tool_results=results) + + bridge = LegacyBridge() + agent = Agent(TestModel(call_tools=["dangerous"])) + + @agent.tool + def dangerous(ctx: RunContext[None], path: str) -> str: + if not ctx.tool_call_approved: + raise ApprovalRequired() + return f"approved:{path}" + + adapter = create_acp_agent( + agent=agent, + config=AdapterConfig( + approval_bridge=bridge, + session_store=MemorySessionStore(), + ), + ) + client = RecordingClient() + adapter.on_connect(client) + + session = asyncio.run(adapter.new_session(cwd=str(tmp_path), mcp_servers=[])) + response = asyncio.run( + adapter.prompt( + prompt=[text_block("Use the dangerous tool.")], + session_id=session.session_id, + ) + ) + + assert response.stop_reason == "end_turn" + assert bridge.called + assert client.permission_option_ids == [] + assert agent_message_texts(client) == ['{"dangerous":"approved:a"}'] + + def test_deferred_approval_write_projection_preserves_pre_write_diff_after_file_changes( tmp_path: Path, ) -> None: diff --git a/tests/pydantic/test_bridge_builder.py b/tests/pydantic/test_bridge_builder.py index 1ed0f98..e8ef9ef 100644 --- a/tests/pydantic/test_bridge_builder.py +++ b/tests/pydantic/test_bridge_builder.py @@ -183,6 +183,7 @@ def search_repo(query: str) -> str: "wrap_model_request", "after_model_request", "prepare_tools", + "prepare_output_tools", "before_tool_validate", "wrap_tool_validate", "after_tool_validate", @@ -190,6 +191,15 @@ def search_repo(query: str) -> str: "wrap_tool_execute", "after_tool_execute", "on_tool_execute_error", + "before_output_validate", + "wrap_output_validate", + "after_output_validate", + "on_output_validate_error", + "before_output_process", + "wrap_output_process", + "after_output_process", + "on_output_process_error", + "handle_deferred_tool_calls", ] } assert metadata["history_processors"] == {"processors": ["trim_history"]} diff --git a/tests/pydantic/test_bridge_capability_support.py b/tests/pydantic/test_bridge_capability_support.py index fd5a610..cac1339 100644 --- a/tests/pydantic/test_bridge_capability_support.py +++ b/tests/pydantic/test_bridge_capability_support.py @@ -1,6 +1,7 @@ from __future__ import annotations as _annotations import asyncio +import os import sys import threading from concurrent.futures import ThreadPoolExecutor @@ -48,6 +49,54 @@ ) +def _write_mcp_stdio_server_script(path: Path) -> None: + path.write_text( + "\n".join( + ( + "from __future__ import annotations as _annotations", + "", + "from mcp.server.fastmcp import FastMCP", + "", + 'mcp = FastMCP("test-mcp", instructions="Be a helpful assistant.")', + "", + "@mcp.tool()", + "def ping() -> str:", + ' return "pong"', + "", + 'if __name__ == "__main__":', + ' mcp.run("stdio")', + "", + ) + ), + encoding="utf-8", + ) + + +def _build_mcp_stdio_test_env( + *, + executable: str, + base_executable: str | None, + sys_path: list[str], + environ: dict[str, str], +) -> tuple[str, dict[str, str]]: + executable_path = Path(executable) + python_executable = ( + str(executable_path) if executable_path.exists() else base_executable or executable + ) + python_path_entries = [entry for entry in sys_path if entry] + mcp_env = dict(environ) + if python_path_entries: + existing_pythonpath = mcp_env.get("PYTHONPATH") + combined_pythonpath = os.pathsep.join( + ( + *python_path_entries, + *([existing_pythonpath] if existing_pythonpath else []), + ) + ) + mcp_env["PYTHONPATH"] = combined_pythonpath + return python_executable, mcp_env + + def test_thread_executor_bridge_runs_sync_tools_on_configured_executor( tmp_path: Path, ) -> None: @@ -381,46 +430,21 @@ def factory(session: AcpSessionContext) -> Agent[None, str]: def test_mcp_toolset_include_instructions_reaches_model_request(tmp_path: Path) -> None: - pytest.importorskip("mcp", exc_type=ImportError) - from pydantic_ai.mcp import MCPServerStdio - - server_script = tmp_path / "mcp_stdio_server.py" - server_script.write_text( - "\n".join( - ( - "from __future__ import annotations as _annotations", - "", - "from mcp.server.fastmcp import FastMCP", - "", - 'mcp = FastMCP("test-mcp", instructions="Be a helpful assistant.")', - "", - "@mcp.tool()", - "def ping() -> str:", - ' return "pong"', - "", - 'if __name__ == "__main__":', - ' mcp.run("stdio")', - "", - ) - ), - encoding="utf-8", - ) - def return_instructions(messages: list[ModelMessage], info: AgentInfo) -> ModelResponse: del messages return ModelResponse(parts=[TextPart(info.instructions or "")]) + toolset = FunctionToolset(instructions="Be a helpful assistant.") + + @toolset.tool_plain + def ping() -> str: + return "pong" + + assert ping() == "pong" + agent = Agent( FunctionModel(return_instructions), - toolsets=[ - MCPServerStdio( - sys.executable, - [str(server_script)], - cwd=tmp_path, - include_instructions=True, - id="mcp", - ) - ], + toolsets=[toolset], ) adapter = create_acp_agent( agent=agent, @@ -444,6 +468,45 @@ def return_instructions(messages: list[ModelMessage], info: AgentInfo) -> ModelR ) +def test_mcp_stdio_test_helpers_cover_script_and_env_fallbacks(tmp_path: Path) -> None: + server_script = tmp_path / "mcp_stdio_server.py" + _write_mcp_stdio_server_script(server_script) + script_text = server_script.read_text(encoding="utf-8") + assert script_text.startswith("from __future__ import annotations as _annotations") + assert 'instructions="Be a helpful assistant."' in script_text + + existing_executable = tmp_path / "python" + existing_executable.write_text("", encoding="utf-8") + existing_python, existing_env = _build_mcp_stdio_test_env( + executable=str(existing_executable), + base_executable="/fallback-python", + sys_path=["/repo/src", "", "/repo/tests"], + environ={"PYTHONPATH": "/already/set"}, + ) + assert existing_python == str(existing_executable) + assert existing_env["PYTHONPATH"] == os.pathsep.join( + ("/repo/src", "/repo/tests", "/already/set") + ) + + missing_python, missing_env = _build_mcp_stdio_test_env( + executable=str(tmp_path / "missing-python"), + base_executable="/fallback-python", + sys_path=[""], + environ={}, + ) + assert missing_python == "/fallback-python" + assert "PYTHONPATH" not in missing_env + + raw_python, raw_env = _build_mcp_stdio_test_env( + executable="/raw-python", + base_executable=None, + sys_path=[], + environ={"PATH": "/usr/bin"}, + ) + assert raw_python == "/raw-python" + assert raw_env == {"PATH": "/usr/bin"} + + def test_capability_bridge_helper_and_metadata_edge_paths( monkeypatch: pytest.MonkeyPatch, ) -> None: diff --git a/tests/pydantic/test_bridge_hooks.py b/tests/pydantic/test_bridge_hooks.py index 0a4d782..7ba91be 100644 --- a/tests/pydantic/test_bridge_hooks.py +++ b/tests/pydantic/test_bridge_hooks.py @@ -12,10 +12,14 @@ UTC, AcpSessionContext, Agent, + ExternalHookEventBridge, HookBridge, + HookEvent, + HookProjectionMap, Path, TestModel, ToolCallProgress, + ToolCallStart, ToolDefinition, datetime, ) @@ -156,6 +160,13 @@ async def ok_validate(args: Any) -> Any: async def ok_execute(args: Any) -> Any: return {"ok": args["text"]} + async def ok_output(output: Any) -> Any: + return output + + async def fail_output(output: Any) -> Any: + del output + raise RuntimeError("output failed") + async def event_stream(): yield cast(Any, SimpleNamespace(event_kind="demo_event")) @@ -289,6 +300,10 @@ async def event_stream(): record_event_stream=False, record_model_requests=False, record_node_lifecycle=False, + record_deferred_tool_calls=False, + record_output_processing=False, + record_output_validation=False, + record_prepare_output_tools=False, record_prepare_tools=False, record_run_lifecycle=False, record_tool_execution=False, @@ -305,6 +320,10 @@ async def event_stream(): partially_disabled.record_node_lifecycle = False partially_disabled.record_event_stream = False partially_disabled.record_model_requests = False + partially_disabled.record_deferred_tool_calls = False + partially_disabled.record_output_processing = False + partially_disabled.record_output_validation = False + partially_disabled.record_prepare_output_tools = False partially_disabled.record_prepare_tools = False partially_disabled.record_tool_validation = False partially_disabled.record_tool_execution = False @@ -331,6 +350,116 @@ async def event_stream(): assert asyncio.run( partially_registry["prepare_tools"][0].func(cast(Any, None), [tool_def]) ) == [tool_def] + assert asyncio.run( + partially_registry["prepare_output_tools"][0].func(cast(Any, None), [tool_def]) + ) == [tool_def] + assert ( + asyncio.run( + partially_registry["before_output_validate"][0].func( + cast(Any, None), + output_context=_HOOK_CONTEXT, + output="raw", + ) + ) + == "raw" + ) + assert ( + asyncio.run( + partially_registry["wrap_output_validate"][0].func( + cast(Any, None), + output_context=_HOOK_CONTEXT, + output="raw", + handler=ok_output, + ) + ) + == "raw" + ) + with pytest.raises(RuntimeError, match="output failed"): + asyncio.run( + partially_registry["wrap_output_validate"][0].func( + cast(Any, None), + output_context=_HOOK_CONTEXT, + output="raw", + handler=fail_output, + ) + ) + assert ( + asyncio.run( + partially_registry["after_output_validate"][0].func( + cast(Any, None), + output_context=_HOOK_CONTEXT, + output="checked", + ) + ) + == "checked" + ) + with pytest.raises(RuntimeError, match="validate failed"): + asyncio.run( + partially_registry["on_output_validate_error"][0].func( + cast(Any, None), + output_context=_HOOK_CONTEXT, + output="raw", + error=RuntimeError("validate failed"), + ) + ) + assert ( + asyncio.run( + partially_registry["before_output_process"][0].func( + cast(Any, None), + output_context=_HOOK_CONTEXT, + output="raw", + ) + ) + == "raw" + ) + assert ( + asyncio.run( + partially_registry["wrap_output_process"][0].func( + cast(Any, None), + output_context=_HOOK_CONTEXT, + output="raw", + handler=ok_output, + ) + ) + == "raw" + ) + with pytest.raises(RuntimeError, match="output failed"): + asyncio.run( + partially_registry["wrap_output_process"][0].func( + cast(Any, None), + output_context=_HOOK_CONTEXT, + output="raw", + handler=fail_output, + ) + ) + assert ( + asyncio.run( + partially_registry["after_output_process"][0].func( + cast(Any, None), + output_context=_HOOK_CONTEXT, + output="processed", + ) + ) + == "processed" + ) + with pytest.raises(RuntimeError, match="process failed"): + asyncio.run( + partially_registry["on_output_process_error"][0].func( + cast(Any, None), + output_context=_HOOK_CONTEXT, + output="raw", + error=RuntimeError("process failed"), + ) + ) + assert ( + asyncio.run( + partially_registry["handle_deferred_tool_calls"][0].func( + cast(Any, None), + requests=cast(Any, None), + ) + ) + is None + ) assert asyncio.run( partially_registry["before_tool_validate"][0].func( cast(Any, None), @@ -366,6 +495,161 @@ def test_hook_bridge_hide_all_disables_registry_and_metadata() -> None: assert bridge.drain_updates(session, Agent(TestModel())) is None +def test_hook_bridge_records_output_and_deferred_hooks() -> None: + bridge = HookBridge() + session = AcpSessionContext( + session_id="hook-output", + cwd=Path("/tmp"), + created_at=datetime.now(UTC), + updated_at=datetime.now(UTC), + ) + registry = bridge.build_capability(session)._registry + tool_def = ToolDefinition(name="result") + + async def ok_output(output: Any) -> Any: + return output + + assert asyncio.run(registry["prepare_output_tools"][0].func(cast(Any, None), [tool_def])) == [ + tool_def + ] + assert ( + asyncio.run( + registry["before_output_validate"][0].func( + cast(Any, None), + output_context=_HOOK_CONTEXT, + output="raw", + ) + ) + == "raw" + ) + assert ( + asyncio.run( + registry["wrap_output_validate"][0].func( + cast(Any, None), + output_context=_HOOK_CONTEXT, + output="raw", + handler=ok_output, + ) + ) + == "raw" + ) + assert asyncio.run( + registry["after_output_validate"][0].func( + cast(Any, None), + output_context=_HOOK_CONTEXT, + output={"ok": True}, + ) + ) == {"ok": True} + assert asyncio.run( + registry["before_output_process"][0].func( + cast(Any, None), + output_context=_HOOK_CONTEXT, + output={"ok": True}, + ) + ) == {"ok": True} + assert asyncio.run( + registry["wrap_output_process"][0].func( + cast(Any, None), + output_context=_HOOK_CONTEXT, + output={"ok": True}, + handler=ok_output, + ) + ) == {"ok": True} + assert ( + asyncio.run( + registry["after_output_process"][0].func( + cast(Any, None), + output_context=_HOOK_CONTEXT, + output="done", + ) + ) + == "done" + ) + assert ( + asyncio.run( + registry["handle_deferred_tool_calls"][0].func( + cast(Any, None), + requests=cast(Any, None), + ) + ) + is None + ) + + updates = bridge.drain_updates(session, Agent(TestModel())) + assert updates is not None + titles = [update.title for update in updates if isinstance(update, ToolCallProgress)] + assert "hook.prepare_output_tools" in titles + assert "hook.wrap_output_validate" in titles + assert "hook.wrap_output_process" in titles + assert "hook.handle_deferred_tool_calls" in titles + + +def test_hook_bridge_output_error_hooks_emit_failed_updates() -> None: + bridge = HookBridge() + session = AcpSessionContext( + session_id="hook-output-errors", + cwd=Path("/tmp"), + created_at=datetime.now(UTC), + updated_at=datetime.now(UTC), + ) + registry = bridge.build_capability(session)._registry + + async def fail_output(output: Any) -> Any: + del output + raise RuntimeError("output failed") + + with pytest.raises(RuntimeError, match="output failed"): + asyncio.run( + registry["wrap_output_validate"][0].func( + cast(Any, None), + output_context=_HOOK_CONTEXT, + output="raw", + handler=fail_output, + ) + ) + with pytest.raises(RuntimeError, match="validate direct"): + asyncio.run( + registry["on_output_validate_error"][0].func( + cast(Any, None), + output_context=_HOOK_CONTEXT, + output="raw", + error=RuntimeError("validate direct"), + ) + ) + with pytest.raises(RuntimeError, match="output failed"): + asyncio.run( + registry["wrap_output_process"][0].func( + cast(Any, None), + output_context=_HOOK_CONTEXT, + output="raw", + handler=fail_output, + ) + ) + with pytest.raises(RuntimeError, match="process direct"): + asyncio.run( + registry["on_output_process_error"][0].func( + cast(Any, None), + output_context=_HOOK_CONTEXT, + output="raw", + error=RuntimeError("process direct"), + ) + ) + + updates = bridge.drain_updates(session, Agent(TestModel())) + assert updates is not None + failed_titles = [ + update.title + for update in updates + if isinstance(update, ToolCallProgress) and update.status == "failed" + ] + assert failed_titles == [ + "hook.wrap_output_validate", + "hook.on_output_validate_error", + "hook.wrap_output_process", + "hook.on_output_process_error", + ] + + def test_hook_bridge_skips_recording_when_flags_are_disabled_after_binding() -> None: bridge = HookBridge() session = AcpSessionContext( @@ -587,3 +871,133 @@ async def event_stream(): ) assert bridge.drain_updates(session, Agent(TestModel(custom_output_text="unused"))) is None + + +def test_external_hook_event_bridge_records_modes_and_metadata() -> None: + session = AcpSessionContext( + session_id="external-hooks", + cwd=Path("/tmp"), + created_at=datetime.now(UTC), + updated_at=datetime.now(UTC), + ) + bridge = ExternalHookEventBridge() + event = HookEvent( + event_id="before_run", + hook_name="before_run", + tool_name=None, + tool_filters=(), + raw_output="done", + status="completed", + ) + + bridge.record_event(session, event) + metadata = bridge.get_session_metadata(session, Agent(TestModel())) + updates = bridge.drain_updates(session, Agent(TestModel())) + + assert metadata["pending_event_count"] == 2 + assert updates is not None + assert [update.tool_call_id for update in updates] == [ + "external-hooks:external-hook:1", + "external-hooks:external-hook:1", + ] + assert isinstance(updates[0], ToolCallStart) + assert isinstance(updates[1], ToolCallProgress) + assert updates[1].status == "completed" + assert bridge.drain_updates(session, Agent(TestModel())) is None + + +def test_external_hook_event_bridge_start_only_and_hidden_events() -> None: + session = AcpSessionContext( + session_id="external-hooks-hidden", + cwd=Path("/tmp"), + created_at=datetime.now(UTC), + updated_at=datetime.now(UTC), + ) + bridge = ExternalHookEventBridge( + projection_map=HookProjectionMap(hidden_event_ids=frozenset({"secret"})), + emission_mode="start_only", + ) + + bridge.record_event( + session, + HookEvent( + event_id="secret", + hook_name="secret", + tool_name=None, + tool_filters=(), + status="completed", + ), + ) + bridge.record_event( + session, + HookEvent( + event_id="before_run", + hook_name="before_run", + tool_name=None, + tool_filters=(), + status="failed", + ), + ) + + updates = bridge.drain_updates(session, Agent(TestModel())) + + assert updates is not None + assert len(updates) == 1 + assert isinstance(updates[0], ToolCallStart) + assert updates[0].status == "failed" + + +def test_external_hook_event_bridge_start_only_preserves_in_progress_without_status() -> None: + session = AcpSessionContext( + session_id="external-hooks-start-only-default-status", + cwd=Path("/tmp"), + created_at=datetime.now(UTC), + updated_at=datetime.now(UTC), + ) + bridge = ExternalHookEventBridge(emission_mode="start_only") + + bridge.record_event( + session, + HookEvent( + event_id="before_run", + hook_name="before_run", + tool_name=None, + tool_filters=(), + status=None, + ), + ) + + updates = bridge.drain_updates(session, Agent(TestModel())) + + assert updates is not None + assert len(updates) == 1 + assert isinstance(updates[0], ToolCallStart) + assert updates[0].status == "in_progress" + + +def test_external_hook_event_bridge_keeps_start_when_progress_is_hidden() -> None: + session = AcpSessionContext( + session_id="external-hooks-start-only-progress-hidden", + cwd=Path("/tmp"), + created_at=datetime.now(UTC), + updated_at=datetime.now(UTC), + ) + bridge = ExternalHookEventBridge() + + bridge.record_event( + session, + HookEvent( + event_id="before_run", + hook_name="before_run", + tool_name=None, + tool_filters=(), + status=None, + ), + ) + + updates = bridge.drain_updates(session, Agent(TestModel())) + + assert updates is not None + assert len(updates) == 1 + assert isinstance(updates[0], ToolCallStart) + assert updates[0].status == "in_progress" diff --git a/tests/pydantic/test_bridge_prepare_tools.py b/tests/pydantic/test_bridge_prepare_tools.py index 9e66518..5aadcfc 100644 --- a/tests/pydantic/test_bridge_prepare_tools.py +++ b/tests/pydantic/test_bridge_prepare_tools.py @@ -1,15 +1,19 @@ from __future__ import annotations as _annotations import asyncio +from collections.abc import Awaitable from typing import Any, cast import pytest +from pydantic_ai.capabilities import PrepareOutputTools from .support import ( UTC, AcpSessionContext, Agent, Path, + PrepareOutputToolsBridge, + PrepareOutputToolsMode, PrepareToolsBridge, PrepareToolsMode, RunContext, @@ -35,6 +39,145 @@ def test_passthrough_tools_helper_returns_a_copy() -> None: assert copied is not tool_defs +def test_prepare_output_tools_bridge_builds_capability_and_metadata(tmp_path: Path) -> None: + def keep_public( + ctx: RunContext[None], + tool_defs: list[ToolDefinition], + ) -> list[ToolDefinition]: + del ctx + return [tool_def for tool_def in tool_defs if tool_def.name == "public"] + + bridge = PrepareOutputToolsBridge( + default_mode_id="default", + modes=[ + PrepareOutputToolsMode( + id="default", + name="Default", + description="Default output tools.", + prepare_func=keep_public, + ), + PrepareOutputToolsMode( + id="strict", + name="Strict", + description=None, + prepare_func=_passthrough_tools, + ), + ], + ) + session = AcpSessionContext( + session_id="prepare-output-tools", + cwd=tmp_path, + created_at=datetime.now(UTC), + updated_at=datetime.now(UTC), + ) + tool_defs = [ToolDefinition(name="public"), ToolDefinition(name="private")] + + async def run_prepare() -> list[ToolDefinition]: + prepared = bridge.build_prepare_output_tools(session) + result = cast(Awaitable[list[ToolDefinition]], prepared(cast(Any, None), tool_defs)) + return await result + + capability = bridge.build_capability(session) + contributions = bridge.build_agent_capabilities(session) + prepared_tools = asyncio.run(run_prepare()) + metadata = bridge.get_session_metadata(session, Agent(TestModel())) + + assert isinstance(capability, PrepareOutputTools) + assert len(contributions) == 1 + assert isinstance(contributions[0], PrepareOutputTools) + assert [tool_def.name for tool_def in prepared_tools] == ["public"] + assert metadata == { + "current_mode_id": "default", + "modes": [ + { + "description": "Default output tools.", + "id": "default", + "name": "Default", + }, + { + "description": None, + "id": "strict", + "name": "Strict", + }, + ], + } + + mode_state = bridge.set_mode(session, Agent(TestModel()), "strict") + assert mode_state is not None + assert mode_state.current_mode_id == "strict" + assert bridge.get_mode_state(session, Agent(TestModel())).current_mode_id == "strict" + session.config_values["prepare_output_tools_mode"] = "missing" + assert bridge.get_mode_state(session, Agent(TestModel())).current_mode_id == "default" + assert bridge.set_mode(session, Agent(TestModel()), "missing") is None + + updates = bridge.drain_updates(session, Agent(TestModel())) + assert updates is not None + + +def test_prepare_output_tools_bridge_validation_and_failure_paths(tmp_path: Path) -> None: + with pytest.raises(ValueError, match="requires at least one mode"): + PrepareOutputToolsBridge(default_mode_id="x", modes=[]) + with pytest.raises(ValueError, match="default mode"): + PrepareOutputToolsBridge( + default_mode_id="missing", + modes=[ + PrepareOutputToolsMode( + id="default", + name="Default", + prepare_func=_passthrough_tools, + ) + ], + ) + with pytest.raises(ValueError, match="reserved slash command names"): + PrepareOutputToolsBridge( + default_mode_id="model", + modes=[ + PrepareOutputToolsMode( + id="model", + name="Model", + prepare_func=_passthrough_tools, + ) + ], + ) + + def boom( + ctx: RunContext[None], + tool_defs: list[ToolDefinition], + ) -> list[ToolDefinition]: + del ctx, tool_defs + raise RuntimeError("output boom") + + bridge = PrepareOutputToolsBridge( + default_mode_id="default", + modes=[ + PrepareOutputToolsMode( + id="default", + name="Default", + prepare_func=boom, + ) + ], + ) + session = AcpSessionContext( + session_id="prepare-output-tools-failure", + cwd=tmp_path, + created_at=datetime.now(UTC), + updated_at=datetime.now(UTC), + ) + + async def run_prepare() -> None: + prepared = bridge.build_prepare_output_tools(session) + result = cast(Awaitable[list[ToolDefinition]], prepared(cast(Any, None), [])) + await result + + with pytest.raises(RuntimeError, match="output boom"): + asyncio.run(run_prepare()) + with pytest.raises(ValueError, match="Unknown prepare output tools mode"): + bridge._require_mode("missing") + + updates = bridge.drain_updates(session, Agent(TestModel())) + assert updates is not None + + def test_prepare_tools_bridge_allows_at_most_one_plan_mode() -> None: with pytest.raises(ValueError, match="at most one `plan_mode=True`"): PrepareToolsBridge( diff --git a/tests/pydantic/test_hook_introspection.py b/tests/pydantic/test_hook_introspection.py index c4c48e5..8d7f47d 100644 --- a/tests/pydantic/test_hook_introspection.py +++ b/tests/pydantic/test_hook_introspection.py @@ -368,6 +368,14 @@ async def beta(ctx, *, call, tool_def, args): del ctx, call, tool_def # pragma: no cover return args # pragma: no cover + async def gamma(ctx, *, output_context, output, handler): + del ctx, output_context # pragma: no cover + return await handler(output) # pragma: no cover + + async def delta(ctx, *, requests): + del ctx, requests # pragma: no cover + return None # pragma: no cover + skipped = _TestHookEntry(skipped_alpha) skipped.func.__module__ = "pydantic_acp.bridges.hooks" tool_entry = _TestHookEntry(beta, tools=frozenset({"z", "a"})) @@ -377,12 +385,16 @@ async def beta(ctx, *, call, tool_def, args): 1: [], "before_model_request": ["bad", broken_entry, skipped, _TestHookEntry(alpha)], "before_tool_execute": [tool_entry], + "handle_deferred_tool_calls": [_TestHookEntry(delta)], + "wrap_output_validate": [_TestHookEntry(gamma)], } listed = list_agent_hooks(Agent(TestModel(custom_output_text="hooked"), capabilities=[hooks])) assert [(info.event_id, info.hook_name, info.tool_filters) for info in listed] == [ ("before_model_request", "alpha", ()), ("before_tool_execute", "beta", ("a", "z")), + ("deferred_tool_calls", "delta", ()), + ("output_validate", "gamma", ()), ] assert _tool_filters(cast(Any, SimpleNamespace(tools=None))) == () assert _tool_name({"call": _INVALID_HOOK_VALUE}) is None diff --git a/tests/pydantic/test_low_level_helpers.py b/tests/pydantic/test_low_level_helpers.py index 8ec7c21..21e46f0 100644 --- a/tests/pydantic/test_low_level_helpers.py +++ b/tests/pydantic/test_low_level_helpers.py @@ -111,7 +111,12 @@ _is_transcript_kind, utc_now, ) -from pydantic_acp.session.store import FileSessionStore, MemorySessionStore, SessionStore +from pydantic_acp.session.store import ( + FileSessionStore, + MemorySessionStore, + SessionStore, + _store_child_path, +) from pydantic_ai import Agent, ModelRequest, ModelResponse, RunUsage, TextPart from pydantic_ai.messages import ( AudioUrl, @@ -381,6 +386,22 @@ def test_file_session_store_covers_delete_fork_missing_and_list_skip( first.title = "First" store.save(first) assert store._session_path("first").exists() + assert store._session_path("safe_1-2").parent == store.root + assert store._temp_session_path("safe_1-2").parent == store.root + with pytest.raises(ValueError, match="session path"): + _store_child_path(store.root, "../escape.json") + + unsafe_session_ids = ["", "../escape", "nested/session", ".hidden", "two words", "x" * 129] + for unsafe_session_id in unsafe_session_ids: + with pytest.raises(ValueError, match="session_id"): + store._session_path(unsafe_session_id) + + with pytest.raises(ValueError, match="session_id"): + store.save(_session(tmp_path, session_id="../escape")) + with pytest.raises(ValueError, match="session_id"): + store.get("../escape") + with pytest.raises(ValueError, match="session_id"): + store.fork("first", new_session_id="../escape", cwd=tmp_path) forked = store.fork("first", new_session_id="forked", cwd=tmp_path / "forked") assert forked is not None @@ -407,6 +428,7 @@ def get(self, session_id: str) -> AcpSessionContext | None: skip_store = SkipGhostFileSessionStore(store.root) ghost_path = skip_store._session_path("ghost") ghost_path.write_text("{}", encoding="utf-8") + (skip_store.root / "bad name.json").write_text("{}", encoding="utf-8") assert skip_store.get("ghost") is None assert skip_store.get("keep") is not None assert [session.session_id for session in skip_store.list_sessions()] == [ diff --git a/tests/pydantic/test_models.py b/tests/pydantic/test_models.py index 4862b8e..8c3046b 100644 --- a/tests/pydantic/test_models.py +++ b/tests/pydantic/test_models.py @@ -1,7 +1,7 @@ from __future__ import annotations as _annotations import asyncio -from typing import Any, cast +from typing import Any, Protocol, cast import pytest from acp.exceptions import RequestError @@ -42,6 +42,10 @@ ) +class _AdapterWithConfig(Protocol): + _config: Any + + def _passthrough_tools( ctx: RunContext[None], tool_defs: list[ToolDefinition], @@ -460,7 +464,8 @@ def route_plan_progress( adapter.on_connect(client) session = asyncio.run(adapter.new_session(cwd=str(tmp_path), mcp_servers=[])) - stored_session = cast(Any, adapter)._config.session_store.get(session.session_id) + adapter_config = cast(_AdapterWithConfig, adapter)._config + stored_session = adapter_config.session_store.get(session.session_id) assert stored_session is not None stored_session.plan_markdown = "# Plan\n\n1. Implement the first item\n2. Verify it\n" stored_session.plan_entries = [ @@ -475,7 +480,7 @@ def route_plan_progress( status="pending", ).model_dump(mode="json"), ] - cast(Any, adapter)._config.session_store.save(stored_session) + adapter_config.session_store.save(stored_session) client.updates.clear() asyncio.run( diff --git a/tests/pydantic/test_projection.py b/tests/pydantic/test_projection.py index 08c52c6..a2ed50e 100644 --- a/tests/pydantic/test_projection.py +++ b/tests/pydantic/test_projection.py @@ -2,11 +2,13 @@ import asyncio from types import SimpleNamespace +from typing import Any import pytest -from acp.schema import ToolCallLocation +from acp.schema import ToolCallLocation, ToolKind from pydantic_acp.projection import ( BuiltinToolProjectionMap, + CompositeProjectionMap, DefaultToolClassifier, ToolProjection, WebToolProjectionMap, @@ -27,8 +29,10 @@ _format_web_search_start, _is_binary_like_content, _json_preview, + _looks_like_status_or_error_output, _preserve_file_diff_content, _read_existing_text, + _render_path_tree, _web_fetch_url, _web_search_query, build_compaction_updates, @@ -54,6 +58,7 @@ FileSystemProjectionMap, MemorySessionStore, Path, + ProjectionAwareToolClassifier, RecordingClient, TerminalToolCallContent, TestModel, @@ -1077,3 +1082,209 @@ def test_build_tool_updates_skips_final_result_and_projects_retry_prompts() -> N assert retry_update.kind == "search" assert isinstance(retry_update.raw_output, str) assert "retry search" in retry_update.raw_output + + +def test_filesystem_search_projection_renders_tree_output() -> None: + projection = FileSystemProjectionMap( + default_search_tool="list_files", + search_path_arg="root", + search_pattern_arg="pattern", + render_search_results_as_tree=True, + ) + + start = projection.project_start( + "list_files", + raw_input={"root": ".", "pattern": "*.py"}, + ) + progress = projection.project_progress( + "list_files", + raw_input={"root": ".", "pattern": "*.py"}, + raw_output="src/app.py\nsrc/utils.py\nREADME.md\n.git/config\n.env\n...", + serialized_output="unused", + status="completed", + ) + + assert start is not None + assert start.title == "Search . for *.py" + assert start.locations is not None + assert start.locations[0].path == "." + assert progress is not None + assert progress.content is not None + text = progress.content[0].content.text + assert "Tree: ." in text + assert "src/" in text + assert "app.py" in text + assert ".env" in text + assert ".git" not in text + assert text.endswith("...") + + +def test_filesystem_search_projection_keeps_plain_output_for_errors_and_disabled_tree() -> None: + projection = FileSystemProjectionMap(default_search_tool="search_files") + + progress = projection.project_progress( + "search_files", + raw_input={"path": "."}, + raw_output="src/app.py", + serialized_output="src/app.py", + status="completed", + ) + failed = projection.project_progress( + "search_files", + raw_input={"path": "."}, + raw_output="error: denied", + serialized_output="error: denied", + status="failed", + ) + + assert progress is not None + assert progress.content is not None + assert progress.content[0].content.text == "src/app.py" + assert failed is not None + assert failed.content is not None + assert failed.content[0].content.text == "error: denied" + + +def test_filesystem_search_projection_covers_edge_paths() -> None: + projection = FileSystemProjectionMap( + default_search_tool="search_files", + search_path_arg="root", + search_pattern_arg="query", + render_search_results_as_tree=True, + hide_dot_directories_in_tree=False, + tree_root_label="workspace", + ) + + assert projection.project_start("search_files", raw_input="invalid") is None + assert ( + projection.project_progress( + "search_files", + raw_input="invalid", + raw_output="src/app.py", + serialized_output="src/app.py", + status="completed", + ) + is None + ) + + pattern_only = projection.project_start("search_files", raw_input={"query": "*.py"}) + path_only = projection.project_start("search_files", raw_input={"root": "src"}) + default_title = projection.project_start("search_files", raw_input={}) + custom_tree = projection.project_progress( + "search_files", + raw_input={"root": "src", "query": "*.py"}, + raw_output=".git/config\nsrc/\n./README.md", + serialized_output="ignored", + status="completed", + ) + + assert pattern_only is not None + assert pattern_only.title == "Search for *.py" + assert path_only is not None + assert path_only.title == "List src" + assert default_title is not None + assert default_title.title == "Search files" + assert custom_tree is not None + assert custom_tree.content is not None + custom_tree_text = custom_tree.content[0].content.text + assert "Tree: workspace" in custom_tree_text + assert ".git/" in custom_tree_text + assert "src/" in custom_tree_text + + +def test_path_tree_helpers_cover_empty_hidden_and_root_paths() -> None: + assert _looks_like_status_or_error_output("") + assert _render_path_tree("\n./\n", root_label=".", hide_dot_directories=True).strip() == "./" + assert ( + _render_path_tree( + ".git/config", + root_label=".", + hide_dot_directories=True, + ) + == ".git/config" + ) + + visible_hidden_dir = _render_path_tree( + ".git/config", + root_label=".", + hide_dot_directories=False, + ) + assert ".git/" in visible_hidden_dir + assert "config" in visible_hidden_dir + + directory_leaf = _render_path_tree( + "src/", + root_label=".", + hide_dot_directories=True, + ) + assert "src/" in directory_leaf + assert _render_path_tree("/", root_label=".", hide_dot_directories=True).startswith("Tree: .") + + +def test_projection_aware_tool_classifier_uses_filesystem_projection_names() -> None: + classifier = ProjectionAwareToolClassifier( + base_classifier=DefaultToolClassifier(), + projection_maps=[ + FileSystemProjectionMap( + default_read_tool="load_document", + default_write_tool="save_document", + default_bash_tool="run_command", + default_search_tool="list_files", + ) + ], + ) + + assert classifier.classify("load_document") == "read" + assert classifier.classify("save_document") == "edit" + assert classifier.classify("run_command") == "execute" + assert classifier.classify("list_files") == "search" + assert classifier.classify("custom_unknown") == "execute" + assert classifier.approval_policy_key("list_files") == "list_files" + + +def test_projection_aware_tool_classifier_preserves_raw_input_for_fallback() -> None: + class ArgSensitiveClassifier: + def classify(self, tool_name: str, raw_input: Any = None) -> ToolKind: + del tool_name + if isinstance(raw_input, dict) and raw_input.get("read_only") is True: + return "read" + return "execute" + + def approval_policy_key(self, tool_name: str, raw_input: Any = None) -> str: + del raw_input + return tool_name + + classifier = ProjectionAwareToolClassifier( + base_classifier=ArgSensitiveClassifier(), + projection_maps=[FileSystemProjectionMap(default_read_tool="known_read")], + ) + + assert classifier.classify("unknown_tool", {"read_only": True}) == "read" + assert classifier.classify("unknown_tool", {"read_only": False}) == "execute" + assert classifier.approval_policy_key("unknown_tool", {"read_only": True}) == "unknown_tool" + + +def test_projection_aware_tool_classifier_recurses_into_composite_maps() -> None: + classifier = ProjectionAwareToolClassifier( + base_classifier=DefaultToolClassifier(), + projection_maps=[ + CompositeProjectionMap( + maps=( + FileSystemProjectionMap(default_search_tool="nested_search"), + CompositeProjectionMap( + maps=( + FileSystemProjectionMap(default_read_tool="nested_read"), + FileSystemProjectionMap(default_write_tool="nested_write"), + FileSystemProjectionMap(default_bash_tool="nested_command"), + ) + ), + ) + ) + ], + ) + + assert classifier.classify("nested_search") == "search" + assert classifier.classify("nested_read") == "read" + assert classifier.classify("nested_write") == "edit" + assert classifier.classify("nested_command") == "execute" + assert classifier.classify("nested_unknown") == "execute" diff --git a/tests/pydantic/test_runtime.py b/tests/pydantic/test_runtime.py index a0ac962..c594c67 100644 --- a/tests/pydantic/test_runtime.py +++ b/tests/pydantic/test_runtime.py @@ -12,6 +12,7 @@ AcpSessionContext, AdapterConfig, AdapterModel, + AdapterPromptCapabilities, Agent, AgentBridgeBuilder, AgentFactory, @@ -46,6 +47,29 @@ ) +def test_initialize_uses_configured_prompt_capabilities(tmp_path: Path) -> None: + adapter = create_acp_agent( + agent=Agent(TestModel(custom_output_text="unused")), + config=AdapterConfig( + prompt_capabilities=AdapterPromptCapabilities( + audio=False, + image=False, + embedded_context=False, + ), + session_store=MemorySessionStore(), + ), + ) + + response = asyncio.run(adapter.initialize(protocol_version=PROTOCOL_VERSION)) + + assert response.agent_capabilities is not None + prompt_capabilities = response.agent_capabilities.prompt_capabilities + assert prompt_capabilities is not None + assert prompt_capabilities.audio is False + assert prompt_capabilities.image is False + assert prompt_capabilities.embedded_context is False + + def test_prompt_and_load_session_replay_history(tmp_path: Path) -> None: adapter = create_acp_agent( agent=Agent(TestModel(custom_output_text="Hello from ACP")), diff --git a/tests/pydantic/test_slash_commands.py b/tests/pydantic/test_slash_commands.py index 7bb91f4..a560001 100644 --- a/tests/pydantic/test_slash_commands.py +++ b/tests/pydantic/test_slash_commands.py @@ -7,8 +7,9 @@ from typing import Any, cast import pytest -from acp.schema import SessionConfigOptionBoolean, SessionMode, SessionModeState +from acp.schema import AvailableCommand, SessionConfigOptionBoolean, SessionMode, SessionModeState from pydantic_acp.runtime.slash_commands import ( + McpServerInfo, _iter_mcp_server_infos, _mcp_server_info_from_bridge_metadata, _mcp_server_info_from_http_toolset, @@ -26,6 +27,7 @@ render_model_message, render_thinking_message, render_tool_listing, + validate_custom_commands, validate_mode_command_ids, ) from pydantic_ai import ModelRequest, ModelResponse, TextPart @@ -52,9 +54,14 @@ PrepareToolsBridge, PrepareToolsMode, RecordingClient, + SlashCommandRequest, + SlashCommandResult, + StaticSlashCommand, + StaticSlashCommandProvider, TestModel, ThinkingBridge, ToolCallProgress, + ToolCallStart, agent_message_texts, create_acp_agent, datetime, @@ -136,6 +143,19 @@ def test_slash_command_render_helpers_cover_empty_states() -> None: == "No Hooks capability callbacks are currently registered." ) assert render_mcp_server_listing([]) == "No MCP servers are currently attached." + assert ( + render_mcp_server_listing( + [ + McpServerInfo( + name="repo", + transport="http", + source="session", + target="https://repo.example/mcp", + ) + ] + ) + == "MCP servers:\n- repo (http, session): https://repo.example/mcp" + ) def test_build_available_commands_skips_optional_commands_when_state_is_missing() -> None: @@ -437,8 +457,9 @@ def test_model_slash_command_accepts_codex_models(tmp_path: Path, monkeypatch) - fake_module = types.ModuleType("codex_auth_helper") fake_model = TestModel(model_name="gpt-5", custom_output_text="codex") - def create_codex_responses_model(model_id: str) -> TestModel: + def create_codex_responses_model(model_id: str, *, instructions: str) -> TestModel: assert model_id == "gpt-5" + assert instructions return fake_model fake_module.__dict__["create_codex_responses_model"] = create_codex_responses_model @@ -559,17 +580,29 @@ def echo(text: str) -> str: def test_mcp_servers_slash_command_extracts_servers_from_agent_toolsets( tmp_path: Path, ) -> None: - pytest.importorskip("mcp", exc_type=ImportError) - from pydantic_ai.capabilities import MCP - from pydantic_ai.mcp import MCPServerSSE, MCPServerStdio + expected_agent_infos = [ + McpServerInfo( + name="remote-sse", + transport="sse", + target="https://example.com/sse | prefix=docs", + source="agent", + ), + McpServerInfo( + name="local-stdio", + transport="stdio", + target="python server.py | prefix=fs", + source="agent", + ), + McpServerInfo( + name="https://example.com/mcp", + transport="http", + target="https://example.com/mcp", + source="agent", + ), + ] agent = Agent( TestModel(custom_output_text="unused"), - capabilities=[MCP("https://example.com/mcp", id="cap-http")], - toolsets=[ - MCPServerSSE("https://example.com/sse", id="remote-sse", tool_prefix="docs"), - MCPServerStdio("python", args=["server.py"], id="local-stdio", tool_prefix="fs"), - ], ) adapter = create_acp_agent( agent=agent, @@ -577,14 +610,22 @@ def test_mcp_servers_slash_command_extracts_servers_from_agent_toolsets( ) client = RecordingClient() adapter.on_connect(client) + monkeypatch = pytest.MonkeyPatch() + monkeypatch.setattr( + "pydantic_acp.runtime.slash_commands.list_agent_mcp_servers", + lambda candidate_agent: expected_agent_infos if candidate_agent is agent else [], + ) - session = asyncio.run(adapter.new_session(cwd=str(tmp_path), mcp_servers=[])) - asyncio.run( - adapter.prompt( - prompt=[text_block("/mcp-servers")], - session_id=session.session_id, + try: + session = asyncio.run(adapter.new_session(cwd=str(tmp_path), mcp_servers=[])) + asyncio.run( + adapter.prompt( + prompt=[text_block("/mcp-servers")], + session_id=session.session_id, + ) ) - ) + finally: + monkeypatch.undo() assert agent_message_texts(client) == [ "MCP servers:\n" @@ -797,10 +838,49 @@ def test_list_agent_mcp_servers_handles_fake_toolsets_and_nested_wrappers() -> N assert _iter_mcp_server_infos(CombinedToolset([])) == [] assert _iter_mcp_server_infos(WrapperToolset(CombinedToolset([]))) == [] assert _iter_mcp_server_infos(DynamicToolset(lambda _ctx: None)) == [] + combined_infos = _iter_mcp_server_infos( + CombinedToolset(cast(Any, [http_toolset, stdio_toolset])) + ) + assert [(info.name, info.transport) for info in combined_infos] == [ + ("remote-http", "http"), + ("local-stdio", "stdio"), + ] dynamic_toolset = DynamicToolset(lambda _ctx: None) dynamic_toolset._toolset = cast(Any, http_toolset) dynamic_infos = _iter_mcp_server_infos(dynamic_toolset) assert [(info.name, info.transport) for info in dynamic_infos] == [("remote-http", "http")] + assert _iter_mcp_server_infos(object()) == [] + + +def test_mcp_servers_slash_command_renders_attached_servers(tmp_path: Path) -> None: + session_store = MemorySessionStore() + adapter = create_acp_agent( + agent=Agent(TestModel(model_name="openai:gpt-5-mini", custom_output_text="unused")), + config=AdapterConfig(session_store=session_store), + ) + client = RecordingClient() + adapter.on_connect(client) + + session = asyncio.run(adapter.new_session(cwd=str(tmp_path), mcp_servers=[])) + stored_session = session_store.get(session.session_id) + assert stored_session is not None + stored_session.mcp_servers = [ + {"name": "repo-http", "transport": "http", "url": "https://repo.example/mcp"}, + { + "name": "repo-stdio", + "transport": "stdio", + "command": "python", + "args": ["server.py"], + }, + ] + session_store.save(stored_session) + asyncio.run(adapter.prompt(prompt=[text_block("/mcp-servers")], session_id=session.session_id)) + + assert agent_message_texts(client) == [ + "MCP servers:\n" + "- repo-http (http, session): https://repo.example/mcp\n" + "- repo-stdio (stdio, session): python server.py" + ] def test_extract_session_mcp_servers_dedupes_agent_and_session_duplicates( @@ -875,3 +955,180 @@ def test_invalid_selected_model_does_not_leave_failed_tool_updates( if isinstance(update, ToolCallProgress) and update.status == "failed" ] assert tool_failures == [] + + +def test_static_custom_slash_command_is_advertised_and_handled(tmp_path: Path) -> None: + provider = StaticSlashCommandProvider( + commands=[ + StaticSlashCommand( + command=AvailableCommand(name="hello", description="Say hello."), + handler=lambda request: SlashCommandResult( + text=f"hello {request.argument}", + updates=[ + ToolCallStart( + session_update="tool_call", + tool_call_id="custom-command", + title="Custom command", + kind="execute", + status="completed", + ) + ], + ), + ) + ] + ) + adapter = create_acp_agent( + agent=Agent(TestModel(custom_output_text="model-output")), + config=AdapterConfig( + session_store=MemorySessionStore(), + slash_command_provider=provider, + ), + ) + client = RecordingClient() + adapter.on_connect(client) + + session = asyncio.run(adapter.new_session(cwd=str(tmp_path), mcp_servers=[])) + asyncio.run(adapter.prompt(prompt=[text_block("/hello world")], session_id=session.session_id)) + asyncio.run(adapter.prompt(prompt=[text_block("/missing")], session_id=session.session_id)) + + command_update = next( + update for _, update in client.updates if isinstance(update, AvailableCommandsUpdate) + ) + assert "hello" in [command.name for command in command_update.available_commands] + assert any( + isinstance(update, ToolCallStart) and update.tool_call_id == "custom-command" + for _, update in client.updates + ) + assert agent_message_texts(client) == ["hello world", "model-output"] + + +def test_custom_slash_command_can_skip_text_and_surface_refresh(tmp_path: Path) -> None: + provider = StaticSlashCommandProvider( + commands=[ + StaticSlashCommand( + command=AvailableCommand(name="silent", description="Emit update only."), + handler=lambda request: SlashCommandResult( + text=None, + updates=[ + ToolCallStart( + session_update="tool_call", + tool_call_id="silent-command", + title="Silent command", + kind="execute", + status="completed", + ) + ], + refresh_session_surface=False, + ), + ) + ] + ) + adapter = create_acp_agent( + agent=Agent(TestModel(custom_output_text="model-output")), + config=AdapterConfig( + session_store=MemorySessionStore(), + slash_command_provider=provider, + ), + ) + client = RecordingClient() + adapter.on_connect(client) + + session = asyncio.run(adapter.new_session(cwd=str(tmp_path), mcp_servers=[])) + client.updates.clear() + asyncio.run(adapter.prompt(prompt=[text_block("/silent")], session_id=session.session_id)) + + assert agent_message_texts(client) == [] + assert [ + update for _, update in client.updates if isinstance(update, AvailableCommandsUpdate) + ] == [] + assert any( + isinstance(update, ToolCallStart) and update.tool_call_id == "silent-command" + for _, update in client.updates + ) + + +def test_custom_slash_command_can_decline_and_fall_through(tmp_path: Path) -> None: + class Provider: + def available_commands(self, session, agent): + del session, agent + return [AvailableCommand(name="maybe", description="Maybe handle.")] + + def handle_command(self, request: SlashCommandRequest) -> SlashCommandResult | None: + del request + return SlashCommandResult(handled=False) + + adapter = create_acp_agent( + agent=Agent(TestModel(custom_output_text="model-output")), + config=AdapterConfig( + session_store=MemorySessionStore(), + slash_command_provider=Provider(), + ), + ) + client = RecordingClient() + adapter.on_connect(client) + + session = asyncio.run(adapter.new_session(cwd=str(tmp_path), mcp_servers=[])) + asyncio.run(adapter.prompt(prompt=[text_block("/maybe")], session_id=session.session_id)) + + assert agent_message_texts(client) == ["model-output"] + + +def test_custom_slash_command_collisions_are_rejected(tmp_path: Path) -> None: + provider = StaticSlashCommandProvider( + commands=[ + StaticSlashCommand( + command=AvailableCommand(name="tools", description="Collides."), + handler=lambda request: SlashCommandResult(text=request.name), + ) + ] + ) + adapter = create_acp_agent( + agent=Agent(TestModel(custom_output_text="unused")), + config=AdapterConfig( + session_store=MemorySessionStore(), + slash_command_provider=provider, + ), + ) + + with pytest.raises(ValueError, match="reserved slash command"): + asyncio.run(adapter.new_session(cwd=str(tmp_path), mcp_servers=[])) + + +def test_validate_custom_commands_rejects_invalid_duplicates_and_mode_collisions() -> None: + with pytest.raises(ValueError, match="must match"): + validate_custom_commands( + [AvailableCommand(name="bad name", description="Invalid.")], + mode_state=None, + ) + + with pytest.raises(ValueError, match="normalized"): + validate_custom_commands( + [AvailableCommand(name=" Demo ", description="Not canonical.")], + mode_state=None, + ) + + with pytest.raises(ValueError, match="unique"): + validate_custom_commands( + [ + AvailableCommand(name="demo", description="First."), + AvailableCommand(name="demo", description="Second."), + ], + mode_state=None, + ) + + with pytest.raises(ValueError, match="active mode ids"): + validate_custom_commands( + [AvailableCommand(name="plan", description="Collides with mode.")], + mode_state=SessionModeState( + current_mode_id="plan", + available_modes=[SessionMode(id=" plan ", name="Plan")], + ), + ) + + validate_custom_commands( + [AvailableCommand(name="review", description="Does not collide.")], + mode_state=SessionModeState( + current_mode_id="plan", + available_modes=[SessionMode(id=" plan ", name="Plan")], + ), + ) diff --git a/tests/pydantic/test_testing_harness.py b/tests/pydantic/test_testing_harness.py index 421007b..0c52274 100644 --- a/tests/pydantic/test_testing_harness.py +++ b/tests/pydantic/test_testing_harness.py @@ -6,6 +6,7 @@ from acp import PROTOCOL_VERSION from pydantic_acp import AdapterConfig, BlackBoxHarness, ClientHostContext, FileSessionStore from pydantic_ai import Agent +from pydantic_ai.exceptions import ApprovalRequired from pydantic_ai.models.test import TestModel from pydantic_ai.tools import RunContext @@ -108,6 +109,35 @@ def test_black_box_harness_covers_initialize_mode_model_and_default_filters( assert harness.agent_messages() == ["provider:model-b"] +def test_black_box_harness_exposes_available_commands_and_permission_requests( + tmp_path: Path, +) -> None: + agent = Agent(TestModel(call_tools=["dangerous"])) + + @agent.tool + def dangerous(ctx: RunContext[None], path: str) -> str: + if not ctx.tool_call_approved: + raise ApprovalRequired() + return f"approved:{path}" # pragma: no cover + + harness = BlackBoxHarness.create( + agent=agent, + config=AdapterConfig(session_store=FileSessionStore(tmp_path / "sessions")), + ) + session = asyncio.run(harness.new_session(cwd=str(tmp_path))) + harness.queue_permission_cancelled() + + response = asyncio.run(harness.prompt_text("Use the dangerous tool.")) + + assert response.stop_reason == "cancelled" + assert "tools" in harness.available_command_names(session_id=session.session_id) + harness.clear_updates() + assert harness.available_command_names(session_id=session.session_id) == [] + assert harness.permission_requests() + assert harness.permission_requests(session_id=session.session_id) + assert harness.last_permission_request(session_id=session.session_id) is not None + + def test_black_box_harness_load_session_returns_none_for_missing_state( tmp_path: Path, ) -> None: diff --git a/tests/test_acpkit_cli.py b/tests/test_acpkit_cli.py index 0360d7a..72265a0 100644 --- a/tests/test_acpkit_cli.py +++ b/tests/test_acpkit_cli.py @@ -5,12 +5,15 @@ import runpy import sys from importlib.machinery import ModuleSpec +from itertools import cycle from pathlib import Path from types import ModuleType, SimpleNamespace from typing import Any, NoReturn, cast import pytest from click.testing import CliRunner +from langchain_core.language_models import GenericFakeChatModel +from langchain_core.messages import AIMessage from pydantic_ai import Agent from acpkit import ( @@ -1253,11 +1256,19 @@ def fake_import_module(name: str, package: str | None = None) -> ModuleType: def test_workspace_graph_module_runs_as_main(monkeypatch: pytest.MonkeyPatch) -> None: observed: dict[str, Any] = {} + import codex_auth_helper import langchain_acp def fake_run_acp(*, graph_factory: Any, config: Any) -> None: observed["call"] = (graph_factory, config) + monkeypatch.setattr( + codex_auth_helper, + "create_codex_chat_openai", + lambda _model_name, **_kwargs: GenericFakeChatModel( + messages=cycle([AIMessage(content="codex-ready")]) + ), + ) monkeypatch.setattr(langchain_acp, "run_acp", fake_run_acp) runpy.run_module("examples.langchain.workspace_graph", run_name="__main__") diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..5b220b4 --- /dev/null +++ b/uv.lock @@ -0,0 +1,2790 @@ +version = 1 +revision = 2 +requires-python = ">=3.11" + +[manifest] +members = [ + "acpkit", + "acpremote", + "codex-auth-helper", + "langchain-acp", + "pydantic-acp", +] + +[[package]] +name = "acpkit" +source = { editable = "." } +dependencies = [ + { name = "click" }, + { name = "typing-extensions" }, +] + +[package.optional-dependencies] +all = [ + { name = "acpremote" }, + { name = "codex-auth-helper" }, + { name = "langchain-acp", extra = ["deepagents"] }, + { name = "pydantic-acp" }, + { name = "uv" }, +] +codex = [ + { name = "codex-auth-helper" }, +] +deepagents = [ + { name = "langchain-acp", extra = ["deepagents"] }, +] +dev = [ + { name = "acpremote" }, + { name = "agent-client-protocol" }, + { name = "basedpyright" }, + { name = "codex-auth-helper" }, + { name = "langchain-acp" }, + { name = "langchain-openai" }, + { name = "pre-commit" }, + { name = "pydantic" }, + { name = "pydantic-acp" }, + { name = "pydantic-ai-slim" }, + { name = "pydantic-graph" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "python-dotenv" }, + { name = "ruff" }, + { name = "ty" }, + { name = "typing-extensions" }, +] +dev-all = [ + { name = "acpremote" }, + { name = "agent-client-protocol" }, + { name = "basedpyright" }, + { name = "codex-auth-helper" }, + { name = "langchain-acp", extra = ["deepagents"] }, + { name = "langchain-openai" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", extra = ["python"] }, + { name = "pre-commit" }, + { name = "pydantic" }, + { name = "pydantic-acp" }, + { name = "pydantic-ai-slim" }, + { name = "pydantic-graph" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "python-dotenv" }, + { name = "ruff" }, + { name = "ty" }, + { name = "typing-extensions" }, + { name = "uv" }, +] +docs = [ + { name = "mkdocs-material" }, + { name = "mkdocstrings", extra = ["python"] }, +] +langchain = [ + { name = "langchain-acp" }, +] +launch = [ + { name = "uv" }, +] +pydantic = [ + { name = "pydantic-acp" }, +] +remote = [ + { name = "acpremote" }, +] + +[package.metadata] +requires-dist = [ + { name = "acpremote", marker = "extra == 'all'", editable = "packages/transports/acpremote" }, + { name = "acpremote", marker = "extra == 'dev'", editable = "packages/transports/acpremote" }, + { name = "acpremote", marker = "extra == 'dev-all'", editable = "packages/transports/acpremote" }, + { name = "acpremote", marker = "extra == 'remote'", editable = "packages/transports/acpremote" }, + { name = "agent-client-protocol", marker = "extra == 'dev'", specifier = ">=0.9.0" }, + { name = "agent-client-protocol", marker = "extra == 'dev-all'", specifier = ">=0.9.0" }, + { name = "basedpyright", marker = "extra == 'dev'" }, + { name = "basedpyright", marker = "extra == 'dev-all'" }, + { name = "click", specifier = ">=8.1.8" }, + { name = "codex-auth-helper", marker = "extra == 'all'", editable = "packages/helpers/codex-auth-helper" }, + { name = "codex-auth-helper", marker = "extra == 'codex'", editable = "packages/helpers/codex-auth-helper" }, + { name = "codex-auth-helper", marker = "extra == 'dev'", editable = "packages/helpers/codex-auth-helper" }, + { name = "codex-auth-helper", marker = "extra == 'dev-all'", editable = "packages/helpers/codex-auth-helper" }, + { name = "langchain-acp", marker = "extra == 'all'", editable = "packages/adapters/langchain-acp" }, + { name = "langchain-acp", marker = "extra == 'dev'", editable = "packages/adapters/langchain-acp" }, + { name = "langchain-acp", marker = "extra == 'dev-all'", editable = "packages/adapters/langchain-acp" }, + { name = "langchain-acp", marker = "extra == 'langchain'", editable = "packages/adapters/langchain-acp" }, + { name = "langchain-acp", extras = ["deepagents"], marker = "extra == 'all'", editable = "packages/adapters/langchain-acp" }, + { name = "langchain-acp", extras = ["deepagents"], marker = "extra == 'deepagents'", editable = "packages/adapters/langchain-acp" }, + { name = "langchain-acp", extras = ["deepagents"], marker = "extra == 'dev-all'", editable = "packages/adapters/langchain-acp" }, + { name = "langchain-openai", marker = "extra == 'dev'", specifier = ">=0.3.26" }, + { name = "langchain-openai", marker = "extra == 'dev-all'", specifier = ">=0.3.26" }, + { name = "mkdocs-material", marker = "extra == 'dev-all'" }, + { name = "mkdocs-material", marker = "extra == 'docs'" }, + { name = "mkdocstrings", extras = ["python"], marker = "extra == 'dev-all'" }, + { name = "mkdocstrings", extras = ["python"], marker = "extra == 'docs'" }, + { name = "pre-commit", marker = "extra == 'dev'" }, + { name = "pre-commit", marker = "extra == 'dev-all'" }, + { name = "pydantic", marker = "extra == 'dev'", specifier = ">=2.7" }, + { name = "pydantic", marker = "extra == 'dev-all'", specifier = ">=2.7" }, + { name = "pydantic-acp", marker = "extra == 'all'", editable = "packages/adapters/pydantic-acp" }, + { name = "pydantic-acp", marker = "extra == 'dev'", editable = "packages/adapters/pydantic-acp" }, + { name = "pydantic-acp", marker = "extra == 'dev-all'", editable = "packages/adapters/pydantic-acp" }, + { name = "pydantic-acp", marker = "extra == 'pydantic'", editable = "packages/adapters/pydantic-acp" }, + { name = "pydantic-ai-slim", marker = "extra == 'dev'" }, + { name = "pydantic-ai-slim", marker = "extra == 'dev-all'" }, + { name = "pydantic-graph", marker = "extra == 'dev'" }, + { name = "pydantic-graph", marker = "extra == 'dev-all'" }, + { name = "pytest", marker = "extra == 'dev'" }, + { name = "pytest", marker = "extra == 'dev-all'" }, + { name = "pytest-asyncio", marker = "extra == 'dev'" }, + { name = "pytest-asyncio", marker = "extra == 'dev-all'" }, + { name = "pytest-cov", marker = "extra == 'dev'" }, + { name = "pytest-cov", marker = "extra == 'dev-all'" }, + { name = "python-dotenv", marker = "extra == 'dev'" }, + { name = "python-dotenv", marker = "extra == 'dev-all'" }, + { name = "ruff", marker = "extra == 'dev'" }, + { name = "ruff", marker = "extra == 'dev-all'" }, + { name = "ty", marker = "extra == 'dev'" }, + { name = "ty", marker = "extra == 'dev-all'" }, + { name = "typing-extensions", specifier = ">=4.12.0" }, + { name = "typing-extensions", marker = "extra == 'dev'", specifier = ">=4.12.0" }, + { name = "typing-extensions", marker = "extra == 'dev-all'", specifier = ">=4.12.0" }, + { name = "uv", marker = "extra == 'all'", specifier = ">=0.8.3" }, + { name = "uv", marker = "extra == 'dev-all'", specifier = ">=0.8.3" }, + { name = "uv", marker = "extra == 'launch'", specifier = ">=0.8.3" }, +] +provides-extras = ["all", "codex", "deepagents", "dev", "dev-all", "docs", "langchain", "launch", "pydantic", "remote"] + +[[package]] +name = "acpremote" +source = { editable = "packages/transports/acpremote" } +dependencies = [ + { name = "agent-client-protocol" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] + +[package.optional-dependencies] +perf = [ + { name = "uvloop", marker = "sys_platform != 'win32'" }, +] + +[package.metadata] +requires-dist = [ + { name = "agent-client-protocol", specifier = ">=0.9.0" }, + { name = "typing-extensions", specifier = ">=4.12.0" }, + { name = "uvloop", marker = "sys_platform != 'win32' and extra == 'perf'", specifier = ">=0.21.0" }, + { name = "websockets", specifier = ">=16.0" }, +] +provides-extras = ["perf"] + +[[package]] +name = "agent-client-protocol" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/13/3b893421369767e7043cc115d6ef0df417c298b84563be3a12df0416158d/agent_client_protocol-0.9.0.tar.gz", hash = "sha256:f744c48ab9af0f0b4452e5ab5498d61bcab97c26dbe7d6feec5fd36de49be30b", size = 71853, upload-time = "2026-03-26T01:21:00.379Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/ed/c284543c08aa443a4ef2c8bd120be51da8433dd174c01749b5d87c333f22/agent_client_protocol-0.9.0-py3-none-any.whl", hash = "sha256:06911500b51d8cb69112544e2be01fc5e7db39ef88fecbc3848c5c6f194798ee", size = 56850, upload-time = "2026-03-26T01:20:59.252Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anthropic" +version = "0.100.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9c/2d/24caf0ff727cba2ed863925017c8f93463a2ea6224a0efe5626e672bc3d2/anthropic-0.100.0.tar.gz", hash = "sha256:650dee9e023afb16395939ee4104bbc21f966b380210119fb91122c12099c79a", size = 758255, upload-time = "2026-05-06T15:07:13.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/a0/c775c59ab9445ecabb57ef3d5c24027de060139189a9e312ef9ef889a665/anthropic-0.100.0-py3-none-any.whl", hash = "sha256:1c15769efa15d8fd5c1ebf900e25c57e3ee540f8554a29aa56e4edefffe2951d", size = 753596, upload-time = "2026-05-06T15:07:12.106Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "backrefs" +version = "7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/a7dd63622beef68cc0d3c3c36d472e143dd95443d5ebf14cd1a5b4dfbf11/backrefs-7.0.tar.gz", hash = "sha256:4989bb9e1e99eb23647c7160ed51fb21d0b41b5d200f2d3017da41e023097e82", size = 7012453, upload-time = "2026-04-28T16:28:04.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/39/39a31d7eae729ea14ed10c3ccef79371197177b9355a86cb3525709e8502/backrefs-7.0-py310-none-any.whl", hash = "sha256:b57cd227ea556b0aed3dc9b8da4628db4eabc0402c6d7fcfc69283a93955f7e9", size = 380824, upload-time = "2026-04-28T16:27:55.647Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b5/9302644225ba7dfa934a2ff2b9c7bb85701313a90dddb3dfaf693fa5bae2/backrefs-7.0-py311-none-any.whl", hash = "sha256:a0fa7360c63509e9e077e174ef4e6d3c21c8db94189b9d957289ae6d794b9475", size = 392626, upload-time = "2026-04-28T16:27:57.42Z" }, + { url = "https://files.pythonhosted.org/packages/36/da/87912ddec6e06feffbaa3d7aa18fc6352bee2e8f1fee185d7d1690f8f4e8/backrefs-7.0-py312-none-any.whl", hash = "sha256:ca42ce6a49ace3d75684dfa9937f3373902a63284ecb385ce36d15e5dcb41c12", size = 398537, upload-time = "2026-04-28T16:27:58.913Z" }, + { url = "https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl", hash = "sha256:f2c52955d631b9e1ac4cd56209f0a3a946d592b98e7790e77699339ae01c102a", size = 400491, upload-time = "2026-04-28T16:28:00.928Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-none-any.whl", hash = "sha256:a6448b28180e3ca01134c9cf09dcebafad8531072e09903c5451748a05f24bc9", size = 412349, upload-time = "2026-04-28T16:28:02.412Z" }, +] + +[[package]] +name = "basedpyright" +version = "1.39.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodejs-wheel-binaries" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/19/5a5b9b9197973da732638957be3a65cf514d2f5a4964eeedbf33b6c65bbd/basedpyright-1.39.3.tar.gz", hash = "sha256:2f794e6b5f4260fb89f614ca6cd23c6f305373bb6b50c4ed7794ff2ae647fb14", size = 25503187, upload-time = "2026-04-20T22:14:47.424Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/5c/f950c1239ad26f3bb453e665428a2cf1893995de725a5eb0b64a2520b366/basedpyright-1.39.3-py3-none-any.whl", hash = "sha256:aba760dc83307727554f936d6b4381caa14482f30dbc2173167710e217c1f7ab", size = 12419181, upload-time = "2026-04-20T22:14:51.975Z" }, +] + +[[package]] +name = "bracex" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/9a/fec38644694abfaaeca2798b58e276a8e61de49e2e37494ace423395febc/bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7", size = 26642, upload-time = "2025-06-22T19:12:31.254Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/2a/9186535ce58db529927f6cf5990a849aa9e052eea3e2cfefe20b9e1802da/bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952", size = 11508, upload-time = "2025-06-22T19:12:29.781Z" }, +] + +[[package]] +name = "certifi" +version = "2026.4.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, +] + +[[package]] +name = "codex-auth-helper" +source = { editable = "packages/helpers/codex-auth-helper" } +dependencies = [ + { name = "httpx" }, + { name = "openai" }, + { name = "pydantic-ai-slim" }, + { name = "typing-extensions" }, +] + +[package.optional-dependencies] +langchain = [ + { name = "langchain-openai" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", specifier = ">=0.28.1" }, + { name = "langchain-openai", marker = "extra == 'langchain'", specifier = ">=0.3.26" }, + { name = "openai", specifier = ">=1.109.1" }, + { name = "pydantic-ai-slim", specifier = ">=1.73.0" }, + { name = "typing-extensions", specifier = ">=4.12.0" }, +] +provides-extras = ["langchain"] + +[[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 = "coverage" +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, + { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" }, + { url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" }, + { url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" }, + { url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" }, + { url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a", size = 250080, upload-time = "2026-03-17T10:30:29.481Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" }, + { url = "https://files.pythonhosted.org/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf", size = 221880, upload-time = "2026-03-17T10:30:36.775Z" }, + { url = "https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9", size = 222816, upload-time = "2026-03-17T10:30:38.891Z" }, + { url = "https://files.pythonhosted.org/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028", size = 221483, upload-time = "2026-03-17T10:30:40.463Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, + { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, + { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, + { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "48.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, + { url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, + { url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, + { url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, + { url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, + { url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, + { url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, + { url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, + { url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, + { url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, + { url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" }, + { url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, + { url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, + { url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, + { url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" }, + { url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" }, + { url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, + { url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, + { url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, + { url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, + { url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, + { url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, + { url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, + { url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" }, + { url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" }, + { url = "https://files.pythonhosted.org/packages/be/d2/024b5e06be9d44cb021fb0e1a03d34d63989cf56a0fe62f3dfbab695b9b4/cryptography-48.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:84cf79f0dc8b36ac5da873481716e87aef31fcfa0444f9e1d8b4b2cece142855", size = 3950391, upload-time = "2026-05-04T22:59:17.415Z" }, + { url = "https://files.pythonhosted.org/packages/bc/17/3861e17c56fa0fd37491a14a8673fdb77c57fc5693cafe745ea8b06dba75/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:fdfef35d751d510fcef5252703621574364fec16418c4a1e5e1055248401054b", size = 4637126, upload-time = "2026-05-04T22:59:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/f0/0a/7e226dbff530f21480727eb764973a7bff2b912f8e15cd4f129e71b56d1d/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:0890f502ddf7d9c6426129c3f49f5c0a39278ed7cd6322c8755ffca6ee675a13", size = 4667270, upload-time = "2026-05-04T22:59:22.647Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f2/5a72274ca9f1b2a8b44a662ee0bf1b435909deb473d6f97bcd035bcdbc71/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:ecde28a596bead48b0cfd2a1b4416c3d43074c2d785e3a398d7ec1fc4d0f7fbb", size = 4636797, upload-time = "2026-05-04T22:59:24.912Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e1/48cedb2fe63626e91ded1edad159e2a4fb8b6906c4425eb7749673077ce7/cryptography-48.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:4defde8685ae324a9eb9d818717e93b4638ef67070ac9bc15b8ca85f63048355", size = 4666800, upload-time = "2026-05-04T22:59:27.474Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ca/7e8365deec19afb2b2c7be7c1c0aa8f99633b54e90c570999acda93260fc/cryptography-48.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:db63bf618e5dea46c07de12e900fe1cdd2541e6dc9dbae772a70b7d4d4765f6a", size = 3739536, upload-time = "2026-05-04T22:59:29.61Z" }, +] + +[[package]] +name = "deepagents" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain" }, + { name = "langchain-anthropic" }, + { name = "langchain-core" }, + { name = "langchain-google-genai" }, + { name = "langsmith" }, + { name = "wcmatch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/81/7c2285e29816a0f1c33211cda31770ddca3b6bb6baf8d645e4d85f133e7d/deepagents-0.5.7.tar.gz", hash = "sha256:8a9e28f2d2c48b5eb1f659573c95829ac31db563cb223d561b0d1dddce133187", size = 165168, upload-time = "2026-05-05T15:54:23.992Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/bb/c1c68ad0ca96cdddd601e7fca0f274be202411378af6da76a35f0e4a1b66/deepagents-0.5.7-py3-none-any.whl", hash = "sha256:2783f87cfeade4c6fbbb86b427e35bf9d12c6cc29d1f0f2b978cd2529082bb66", size = 187645, upload-time = "2026-05-05T15:54:22.764Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, +] + +[[package]] +name = "filelock" +version = "3.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, +] + +[[package]] +name = "filetype" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020, upload-time = "2022-11-02T17:34:04.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970, upload-time = "2022-11-02T17:34:01.425Z" }, +] + +[[package]] +name = "genai-prices" +version = "0.0.59" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/c8/b61a028b8d8ee286ffab3f9b9f1c9229087184e7d543cea4e349e11375b0/genai_prices-0.0.59.tar.gz", hash = "sha256:3e1c7dcd9b38163589c8cf4a9bcfd286c52ea57a3becdc062a2cbaa8295b08c4", size = 67406, upload-time = "2026-05-07T12:08:40.475Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/f9/4693c127f9fab0a8d39c47c198e378ecafcb043463e6dd73df205eacbc13/genai_prices-0.0.59-py3-none-any.whl", hash = "sha256:88fd8818e6807374e5a5c03f293b574ade5f18a3060622080cdd94a03cf43115", size = 70509, upload-time = "2026-05-07T12:08:39.075Z" }, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "google-auth" +version = "2.52.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/f8/80d2493cbedece1c623dc3e3cb1883300871af0dcdae254409522985ac23/google_auth-2.52.0.tar.gz", hash = "sha256:01f30e1a9e3638698d89464f5e603ce29d18e1c0e63ec31ac570aba4e164aaf5", size = 335027, upload-time = "2026-05-07T19:45:24.033Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/fc/2cdc74252746f547f81ff3f02d4d4234a3f411b5de5b61af97e633a060b9/google_auth-2.52.0-py3-none-any.whl", hash = "sha256:aee92803ba0ff93a70a3b8a35c7b4797837751cd6380b63ff38372b98f3ed627", size = 245614, upload-time = "2026-05-07T19:45:21.914Z" }, +] + +[package.optional-dependencies] +requests = [ + { name = "requests" }, +] + +[[package]] +name = "google-genai" +version = "1.75.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "google-auth", extra = ["requests"] }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "sniffio" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/59/3ed61240ef20b3ae6ed54e82c6f8b6d1f194947bc6679679dd6cdb037594/google_genai-1.75.0.tar.gz", hash = "sha256:56bac3991b311c93f980c0a2abcd287b672146905df1fbd71c92ed633d5a07cf", size = 539039, upload-time = "2026-05-04T22:48:54.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/b6/552d40e96da22921eb1fead7c14b00b5b5473a20e45959488660fab35ee2/google_genai-1.75.0-py3-none-any.whl", hash = "sha256:8dc4c096e7d6288c3087f6893f582fe52468932464781edb8193bd92b9fefb2c", size = 793726, upload-time = "2026-05-04T22:48:53.033Z" }, +] + +[[package]] +name = "griffelib" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, +] + +[[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 = "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 = "identify" +version = "2.6.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, +] + +[[package]] +name = "idna" +version = "3.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[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 = "jiter" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6e/c1/0cddc6eb17d4c53a99840953f95dd3accdc5cfc7a337b0e9b26476276be9/jiter-0.14.0.tar.gz", hash = "sha256:e8a39e66dac7153cf3f964a12aad515afa8d74938ec5cc0018adcdae5367c79e", size = 165725, upload-time = "2026-04-10T14:28:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/1f/198ae537fccb7080a0ed655eb56abf64a92f79489dfbf79f40fa34225bcd/jiter-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:7e791e247b8044512e070bd1f3633dc08350d32776d2d6e7473309d0edf256a2", size = 316896, upload-time = "2026-04-10T14:26:01.986Z" }, + { url = "https://files.pythonhosted.org/packages/cf/34/da67cff3fce964a36d03c3e365fb0f8726ade2a6cfd4d3c70107e216ead6/jiter-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71527ce13fd5a0c4e40ad37331f8c547177dbb2dd0a93e5278b6a5eecf748804", size = 321085, upload-time = "2026-04-10T14:26:03.364Z" }, + { url = "https://files.pythonhosted.org/packages/ed/36/4c72e67180d4e71a4f5dcf7886d0840e83c49ab11788172177a77570326e/jiter-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02c4a7ab56f746014874f2c525584c0daca1dec37f66fd707ecef3b7e5c2228c", size = 347393, upload-time = "2026-04-10T14:26:05.314Z" }, + { url = "https://files.pythonhosted.org/packages/bc/db/9b39e09ceafa9878235c0fc29e3e3f9b12a4c6a98ea3085b998cadf3accc/jiter-0.14.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:376e9dafff914253bb9d46cdc5f7965607fbe7feb0a491c34e35f92b2770702e", size = 372937, upload-time = "2026-04-10T14:26:06.884Z" }, + { url = "https://files.pythonhosted.org/packages/b0/96/0dcba1d7a82c1b720774b48ef239376addbaf30df24c34742ac4a57b67b2/jiter-0.14.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23ad2a7a9da1935575c820428dd8d2490ce4d23189691ce33da1fc0a58e14e1c", size = 463646, upload-time = "2026-04-10T14:26:08.345Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e3/f61b71543e746e6b8b805e7755814fc242715c16f1dba58e1cbccb8032c2/jiter-0.14.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54b3ddf5786bc7732d293bba3411ac637ecfa200a39983166d1df86a59a43c9f", size = 380225, upload-time = "2026-04-10T14:26:10.161Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5e/0ddeb7096aca099114abe36c4921016e8d251e6f35f5890240b31f1f60ae/jiter-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c001d5a646c2a50dc055dd526dad5d5245969e8234d2b1131d0451e81f3a373", size = 358682, upload-time = "2026-04-10T14:26:11.574Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d1/fe0c46cd7fda9cad8f1ff9ad217dc61f1e4280b21052ec6dfe88c1446ef2/jiter-0.14.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:834bb5bdabca2e91592a03d373838a8d0a1b8bbde7077ae6913fd2fc51812d00", size = 359973, upload-time = "2026-04-10T14:26:13.316Z" }, + { url = "https://files.pythonhosted.org/packages/ac/21/f5317f91729b501019184771c80d60abd89907009e7bfa6c7e348c5bdd44/jiter-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4e9178be60e229b1b2b0710f61b9e24d1f4f8556985a83ff4c4f95920eea7314", size = 397568, upload-time = "2026-04-10T14:26:15.212Z" }, + { url = "https://files.pythonhosted.org/packages/e9/05/79d8f33fb2bf168db0df5c9cd16fe440a8ada57e929d3677b22712c2568f/jiter-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a7e4ccff04ec03614e62c613e976a3a5860dc9714ce8266f44328bdc8b1cab2c", size = 522535, upload-time = "2026-04-10T14:26:16.956Z" }, + { url = "https://files.pythonhosted.org/packages/5c/00/d1e3ff3d2a465e67f08507d74bafb2dcd29eba91dc939820e39e8dea38b8/jiter-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:69539d936fb5d55caf6ecd33e2e884de083ff0ea28579780d56c4403094bb8d9", size = 556709, upload-time = "2026-04-10T14:26:18.5Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/bbb2189f62ace8d95e869aa4c84c9946616f301e2d02895a6f20dcc3bba3/jiter-0.14.0-cp311-cp311-win32.whl", hash = "sha256:4927d09b3e572787cc5e0a5318601448e1ab9391bcef95677f5840c2d00eaa6d", size = 208660, upload-time = "2026-04-10T14:26:20.511Z" }, + { url = "https://files.pythonhosted.org/packages/b8/86/c500b53dcbf08575f5963e536ebd757a1f7c568272ba5d180b212c9a87fb/jiter-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:42d6ed359ac49eb922fdd565f209c57340aa06d589c84c8413e42a0f9ae1b842", size = 204659, upload-time = "2026-04-10T14:26:22.152Z" }, + { url = "https://files.pythonhosted.org/packages/75/4a/a676249049d42cb29bef82233e4fe0524d414cbe3606c7a4b311193c2f77/jiter-0.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:6dd689f5f4a5a33747b28686e051095beb214fe28cfda5e9fe58a295a788f593", size = 194772, upload-time = "2026-04-10T14:26:23.458Z" }, + { url = "https://files.pythonhosted.org/packages/5a/68/7390a418f10897da93b158f2d5a8bd0bcd73a0f9ec3bb36917085bb759ef/jiter-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:2fb2ce3a7bc331256dfb14cefc34832366bb28a9aca81deaf43bbf2a5659e607", size = 316295, upload-time = "2026-04-10T14:26:24.887Z" }, + { url = "https://files.pythonhosted.org/packages/60/a0/5854ac00ff63551c52c6c89534ec6aba4b93474e7924d64e860b1c94165b/jiter-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5252a7ca23785cef5d02d4ece6077a1b556a410c591b379f82091c3001e14844", size = 315898, upload-time = "2026-04-10T14:26:26.601Z" }, + { url = "https://files.pythonhosted.org/packages/41/a1/4f44832650a16b18e8391f1bf1d6ca4909bc738351826bcc198bba4357f4/jiter-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c409578cbd77c338975670ada777add4efd53379667edf0aceea730cabede6fb", size = 343730, upload-time = "2026-04-10T14:26:28.326Z" }, + { url = "https://files.pythonhosted.org/packages/48/64/a329e9d469f86307203594b1707e11ae51c3348d03bfd514a5f997870012/jiter-0.14.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ede4331a1899d604463369c730dbb961ffdc5312bc7f16c41c2896415b1304a", size = 370102, upload-time = "2026-04-10T14:26:30.089Z" }, + { url = "https://files.pythonhosted.org/packages/94/c1/5e3dfc59635aa4d4c7bd20a820ac1d09b8ed851568356802cf1c08edb3cf/jiter-0.14.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92cd8b6025981a041f5310430310b55b25ca593972c16407af8837d3d7d2ca01", size = 461335, upload-time = "2026-04-10T14:26:31.911Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1b/dd157009dbc058f7b00108f545ccb72a2d56461395c4fc7b9cfdccb00af4/jiter-0.14.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:351bf6eda4e3a7ceb876377840c702e9a3e4ecc4624dbfb2d6463c67ae52637d", size = 378536, upload-time = "2026-04-10T14:26:33.595Z" }, + { url = "https://files.pythonhosted.org/packages/91/78/256013667b7c10b8834f8e6e54cd3e562d4c6e34227a1596addccc05e38c/jiter-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dcfbeb93d9ecd9ca128bbf8910120367777973fa193fb9a39c31237d8df165", size = 353859, upload-time = "2026-04-10T14:26:35.098Z" }, + { url = "https://files.pythonhosted.org/packages/de/d9/137d65ade9093a409fe80955ce60b12bb753722c986467aeda47faf450ad/jiter-0.14.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ae039aaef8de3f8157ecc1fdd4d85043ac4f57538c245a0afaecb8321ec951c3", size = 357626, upload-time = "2026-04-10T14:26:36.685Z" }, + { url = "https://files.pythonhosted.org/packages/2e/48/76750835b87029342727c1a268bea8878ab988caf81ee4e7b880900eeb5a/jiter-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7d9d51eb96c82a9652933bd769fe6de66877d6eb2b2440e281f2938c51b5643e", size = 393172, upload-time = "2026-04-10T14:26:38.097Z" }, + { url = "https://files.pythonhosted.org/packages/a6/60/456c4e81d5c8045279aefe60e9e483be08793828800a4e64add8fdde7f2a/jiter-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d824ca4148b705970bf4e120924a212fdfca9859a73e42bd7889a63a4ea6bb98", size = 520300, upload-time = "2026-04-10T14:26:39.532Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9f/2020e0984c235f678dced38fe4eec3058cf528e6af36ebf969b410305941/jiter-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ff3a6465b3a0f54b1a430f45c3c0ba7d61ceb45cbc3e33f9e1a7f638d690baf3", size = 553059, upload-time = "2026-04-10T14:26:40.991Z" }, + { url = "https://files.pythonhosted.org/packages/ef/32/e2d298e1a22a4bbe6062136d1c7192db7dba003a6975e51d9a9eecabc4c2/jiter-0.14.0-cp312-cp312-win32.whl", hash = "sha256:5dec7c0a3e98d2a3f8a2e67382d0d7c3ac60c69103a4b271da889b4e8bb1e129", size = 206030, upload-time = "2026-04-10T14:26:42.517Z" }, + { url = "https://files.pythonhosted.org/packages/36/ac/96369141b3d8a4a8e4590e983085efe1c436f35c0cda940dd76d942e3e40/jiter-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:fc7e37b4b8bc7e80a63ad6cfa5fc11fab27dbfea4cc4ae644b1ab3f273dc348f", size = 201603, upload-time = "2026-04-10T14:26:44.328Z" }, + { url = "https://files.pythonhosted.org/packages/01/c3/75d847f264647017d7e3052bbcc8b1e24b95fa139c320c5f5066fa7a0bdd/jiter-0.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:ee4a72f12847ef29b072aee9ad5474041ab2924106bdca9fcf5d7d965853e057", size = 191525, upload-time = "2026-04-10T14:26:46Z" }, + { url = "https://files.pythonhosted.org/packages/97/2a/09f70020898507a89279659a1afe3364d57fc1b2c89949081975d135f6f5/jiter-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:af72f204cf4d44258e5b4c1745130ac45ddab0e71a06333b01de660ab4187a94", size = 315502, upload-time = "2026-04-10T14:26:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/d6/be/080c96a45cd74f9fce5db4fd68510b88087fb37ffe2541ff73c12db92535/jiter-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4b77da71f6e819be5fbcec11a453fde5b1d0267ef6ed487e2a392fd8e14e4e3a", size = 314870, upload-time = "2026-04-10T14:26:49.149Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5e/2d0fee155826a968a832cc32438de5e2a193292c8721ca70d0b53e58245b/jiter-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f4ea612fe8b84b8b04e51d0e78029ecf3466348e25973f953de6e6a59aa4c1", size = 343406, upload-time = "2026-04-10T14:26:50.762Z" }, + { url = "https://files.pythonhosted.org/packages/70/af/bf9ee0d3a4f8dc0d679fc1337f874fe60cdbf841ebbb304b374e1c9aaceb/jiter-0.14.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:62fe2451f8fcc0240261e6a4df18ecbcd58327857e61e625b2393ea3b468aac9", size = 369415, upload-time = "2026-04-10T14:26:52.188Z" }, + { url = "https://files.pythonhosted.org/packages/0f/83/8e8561eadba31f4d3948a5b712fb0447ec71c3560b57a855449e7b8ddc98/jiter-0.14.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6112f26f5afc75bcb475787d29da3aa92f9d09c7858f632f4be6ffe607be82e9", size = 461456, upload-time = "2026-04-10T14:26:53.611Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c9/c5299e826a5fe6108d172b344033f61c69b1bb979dd8d9ddd4278a160971/jiter-0.14.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:215a6cb8fb7dc702aa35d475cc00ddc7f970e5c0b1417fb4b4ac5d82fa2a29db", size = 378488, upload-time = "2026-04-10T14:26:55.211Z" }, + { url = "https://files.pythonhosted.org/packages/5d/37/c16d9d15c0a471b8644b1abe3c82668092a707d9bedcf076f24ff2e380cd/jiter-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ab96a30fb3cb2c7e0cd33f7616c8860da5f5674438988a54ac717caccdbaa", size = 353242, upload-time = "2026-04-10T14:26:56.705Z" }, + { url = "https://files.pythonhosted.org/packages/58/ea/8050cb0dc654e728e1bfacbc0c640772f2181af5dedd13ae70145743a439/jiter-0.14.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:3a99c1387b1f2928f799a9de899193484d66206a50e98233b6b088a7f0c1edb2", size = 356823, upload-time = "2026-04-10T14:26:58.281Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/cf71506d270e5f84d97326bf220e47aed9b95e9a4a060758fb07772170ab/jiter-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ab18d11074485438695f8d34a1b6da61db9754248f96d51341956607a8f39985", size = 392564, upload-time = "2026-04-10T14:27:00.018Z" }, + { url = "https://files.pythonhosted.org/packages/b0/cc/8c6c74a3efb5bd671bfd14f51e8a73375464ca914b1551bc3b40e26ac2c9/jiter-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:801028dcfc26ac0895e4964cbc0fd62c73be9fd4a7d7b1aaf6e5790033a719b7", size = 520322, upload-time = "2026-04-10T14:27:01.664Z" }, + { url = "https://files.pythonhosted.org/packages/41/24/68d7b883ec959884ddf00d019b2e0e82ba81b167e1253684fa90519ce33c/jiter-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ad425b087aafb4a1c7e1e98a279200743b9aaf30c3e0ba723aec93f061bd9bc8", size = 552619, upload-time = "2026-04-10T14:27:03.316Z" }, + { url = "https://files.pythonhosted.org/packages/b6/89/b1a0985223bbf3150ff9e8f46f98fc9360c1de94f48abe271bbe1b465682/jiter-0.14.0-cp313-cp313-win32.whl", hash = "sha256:882bcb9b334318e233950b8be366fe5f92c86b66a7e449e76975dfd6d776a01f", size = 205699, upload-time = "2026-04-10T14:27:04.662Z" }, + { url = "https://files.pythonhosted.org/packages/4c/19/3f339a5a7f14a11730e67f6be34f9d5105751d547b615ef593fa122a5ded/jiter-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:9b8c571a5dba09b98bd3462b5a53f27209a5cbbe85670391692ede71974e979f", size = 201323, upload-time = "2026-04-10T14:27:06.139Z" }, + { url = "https://files.pythonhosted.org/packages/50/56/752dd89c84be0e022a8ea3720bcfa0a8431db79a962578544812ce061739/jiter-0.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:34f19dcc35cb1abe7c369b3756babf8c7f04595c0807a848df8f26ef8298ef92", size = 191099, upload-time = "2026-04-10T14:27:07.564Z" }, + { url = "https://files.pythonhosted.org/packages/91/28/292916f354f25a1fe8cf2c918d1415c699a4a659ae00be0430e1c5d9ffea/jiter-0.14.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e89bcd7d426a75bb4952c696b267075790d854a07aad4c9894551a82c5b574ab", size = 320880, upload-time = "2026-04-10T14:27:09.326Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c7/b002a7d8b8957ac3d469bd59c18ef4b1595a5216ae0de639a287b9816023/jiter-0.14.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b25beaa0d4447ea8c7ae0c18c688905d34840d7d0b937f2f7bdd52162c98a40", size = 346563, upload-time = "2026-04-10T14:27:11.287Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3b/f8d07580d8706021d255a6356b8fab13ee4c869412995550ce6ed4ddf97d/jiter-0.14.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:651a8758dd413c51e3b7f6557cdc6921faf70b14106f45f969f091f5cda990ea", size = 357928, upload-time = "2026-04-10T14:27:12.729Z" }, + { url = "https://files.pythonhosted.org/packages/47/5b/ac1a974da29e35507230383110ffec59998b290a8732585d04e19a9eb5ba/jiter-0.14.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e1a7eead856a5038a8d291f1447176ab0b525c77a279a058121b5fccee257f6f", size = 203519, upload-time = "2026-04-10T14:27:14.125Z" }, + { url = "https://files.pythonhosted.org/packages/96/6d/9fc8433d667d2454271378a79747d8c76c10b51b482b454e6190e511f244/jiter-0.14.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e692633a12cda97e352fdcd1c4acc971b1c28707e1e33aeef782b0cbf051975", size = 190113, upload-time = "2026-04-10T14:27:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/4f/1e/354ed92461b165bd581f9ef5150971a572c873ec3b68a916d5aa91da3cc2/jiter-0.14.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:6f396837fc7577871ca8c12edaf239ed9ccef3bbe39904ae9b8b63ce0a48b140", size = 315277, upload-time = "2026-04-10T14:27:18.109Z" }, + { url = "https://files.pythonhosted.org/packages/a6/95/8c7c7028aa8636ac21b7a55faef3e34215e6ed0cbf5ae58258427f621aa3/jiter-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a4d50ea3d8ba4176f79754333bd35f1bbcd28e91adc13eb9b7ca91bc52a6cef9", size = 315923, upload-time = "2026-04-10T14:27:19.603Z" }, + { url = "https://files.pythonhosted.org/packages/47/40/e2a852a44c4a089f2681a16611b7ce113224a80fd8504c46d78491b47220/jiter-0.14.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce17f8a050447d1b4153bda4fb7d26e6a9e74eb4f4a41913f30934c5075bf615", size = 344943, upload-time = "2026-04-10T14:27:21.262Z" }, + { url = "https://files.pythonhosted.org/packages/fc/1f/670f92adee1e9895eac41e8a4d623b6da68c4d46249d8b556b60b63f949e/jiter-0.14.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4f1c4b125e1652aefbc2e2c1617b60a160ab789d180e3d423c41439e5f32850", size = 369725, upload-time = "2026-04-10T14:27:22.766Z" }, + { url = "https://files.pythonhosted.org/packages/01/2f/541c9ba567d05de1c4874a0f8f8c5e3fd78e2b874266623da9a775cf46e0/jiter-0.14.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be808176a6a3a14321d18c603f2d40741858a7c4fc982f83232842689fe86dd9", size = 461210, upload-time = "2026-04-10T14:27:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a9/c31cbec09627e0d5de7aeaec7690dba03e090caa808fefd8133137cf45bc/jiter-0.14.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26679d58ba816f88c3849306dd58cb863a90a1cf352cdd4ef67e30ccf8a77994", size = 380002, upload-time = "2026-04-10T14:27:26.155Z" }, + { url = "https://files.pythonhosted.org/packages/50/02/3c05c1666c41904a2f607475a73e7a4763d1cbde2d18229c4f85b22dc253/jiter-0.14.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80381f5a19af8fa9aef743f080e34f6b25ebd89656475f8cf0470ec6157052aa", size = 354678, upload-time = "2026-04-10T14:27:27.701Z" }, + { url = "https://files.pythonhosted.org/packages/7d/97/e15b33545c2b13518f560d695f974b9891b311641bdcf178d63177e8801e/jiter-0.14.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:004df5fdb8ecbd6d99f3227df18ba1a259254c4359736a2e6f036c944e02d7c5", size = 358920, upload-time = "2026-04-10T14:27:29.256Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d2/8b1461def6b96ba44530df20d07ef7a1c7da22f3f9bf1727e2d611077bf1/jiter-0.14.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cff5708f7ed0fa098f2b53446c6fa74c48469118e5cd7497b4f1cd569ab06928", size = 394512, upload-time = "2026-04-10T14:27:31.344Z" }, + { url = "https://files.pythonhosted.org/packages/e3/88/837566dd6ed6e452e8d3205355afd484ce44b2533edfa4ed73a298ea893e/jiter-0.14.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:2492e5f06c36a976d25c7cc347a60e26d5470178d44cde1b9b75e60b4e519f28", size = 521120, upload-time = "2026-04-10T14:27:33.299Z" }, + { url = "https://files.pythonhosted.org/packages/89/6b/b00b45c4d1b4c031777fe161d620b755b5b02cdade1e316dcb46e4471d63/jiter-0.14.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:7609cfbe3a03d37bfdbf5052012d5a879e72b83168a363deae7b3a26564d57de", size = 553668, upload-time = "2026-04-10T14:27:34.868Z" }, + { url = "https://files.pythonhosted.org/packages/ad/d8/6fe5b42011d19397433d345716eac16728ac241862a2aac9c91923c7509a/jiter-0.14.0-cp314-cp314-win32.whl", hash = "sha256:7282342d32e357543565286b6450378c3cd402eea333fc1ebe146f1fabb306fc", size = 207001, upload-time = "2026-04-10T14:27:36.455Z" }, + { url = "https://files.pythonhosted.org/packages/e5/43/5c2e08da1efad5e410f0eaaabeadd954812612c33fbbd8fd5328b489139d/jiter-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:bd77945f38866a448e73b0b7637366afa814d4617790ecd88a18ca74377e6c02", size = 202187, upload-time = "2026-04-10T14:27:38Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1f/6e39ac0b4cdfa23e606af5b245df5f9adaa76f35e0c5096790da430ca506/jiter-0.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:f2d4c61da0821ee42e0cdf5489da60a6d074306313a377c2b35af464955a3611", size = 192257, upload-time = "2026-04-10T14:27:39.504Z" }, + { url = "https://files.pythonhosted.org/packages/05/57/7dbc0ffbbb5176a27e3518716608aa464aee2e2887dc938f0b900a120449/jiter-0.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1bf7ff85517dd2f20a5750081d2b75083c1b269cf75afc7511bdf1f9548beb3b", size = 323441, upload-time = "2026-04-10T14:27:41.039Z" }, + { url = "https://files.pythonhosted.org/packages/83/6e/7b3314398d8983f06b557aa21b670511ec72d3b79a68ee5e4d9bff972286/jiter-0.14.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8ef8791c3e78d6c6b157c6d360fbb5c715bebb8113bc6a9303c5caff012754a", size = 348109, upload-time = "2026-04-10T14:27:42.552Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4f/8dc674bcd7db6dba566de73c08c763c337058baff1dbeb34567045b27cdc/jiter-0.14.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e74663b8b10da1fe0f4e4703fd7980d24ad17174b6bb35d8498d6e3ebce2ae6a", size = 368328, upload-time = "2026-04-10T14:27:44.574Z" }, + { url = "https://files.pythonhosted.org/packages/3b/5f/188e09a1f20906f98bbdec44ed820e19f4e8eb8aff88b9d1a5a497587ff3/jiter-0.14.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1aca29ba52913f78362ec9c2da62f22cdc4c3083313403f90c15460979b84d9b", size = 463301, upload-time = "2026-04-10T14:27:46.717Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f0/19046ef965ed8f349e8554775bb12ff4352f443fbe12b95d31f575891256/jiter-0.14.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b39b7d87a952b79949af5fef44d2544e58c21a28da7f1bae3ef166455c61746", size = 378891, upload-time = "2026-04-10T14:27:48.32Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c3/da43bd8431ee175695777ee78cf0e93eacbb47393ff493f18c45231b427d/jiter-0.14.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d918a68b26e9fab068c2b5453577ef04943ab2807b9a6275df2a812599a310", size = 360749, upload-time = "2026-04-10T14:27:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/72/26/e054771be889707c6161dbdec9c23d33a9ec70945395d70f07cfea1e9a6f/jiter-0.14.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:b08997c35aee1201c1a5361466a8fb9162d03ae7bf6568df70b6c859f1e654a4", size = 358526, upload-time = "2026-04-10T14:27:51.504Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0f/7bea65ea2a6d91f2bf989ff11a18136644392bf2b0497a1fa50934c30a9c/jiter-0.14.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:260bf7ca20704d58d41f669e5e9fe7fe2fa72901a6b324e79056f5d52e9c9be2", size = 393926, upload-time = "2026-04-10T14:27:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/b1ff7d70deef61ac0b7c6c2f12d2ace950cdeecb4fdc94500a0926802857/jiter-0.14.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:37826e3df29e60f30a382f9294348d0238ef127f4b5d7f5f8da78b5b9e050560", size = 521052, upload-time = "2026-04-10T14:27:55.058Z" }, + { url = "https://files.pythonhosted.org/packages/0b/7b/3b0649983cbaf15eda26a414b5b1982e910c67bd6f7b1b490f3cfc76896a/jiter-0.14.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:645be49c46f2900937ba0eaf871ad5183c96858c0af74b6becc7f4e367e36e06", size = 553716, upload-time = "2026-04-10T14:27:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/97/f8/33d78c83bd93ae0c0af05293a6660f88a1977caef39a6d72a84afab94ce0/jiter-0.14.0-cp314-cp314t-win32.whl", hash = "sha256:2f7877ed45118de283786178eceaf877110abacd04fde31efff3940ae9672674", size = 207957, upload-time = "2026-04-10T14:27:59.285Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ac/2b760516c03e2227826d1f7025d89bf6bf6357a28fe75c2a2800873c50bf/jiter-0.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:14c0cb10337c49f5eafe8e7364daca5e29a020ea03580b8f8e6c597fed4e1588", size = 204690, upload-time = "2026-04-10T14:28:00.962Z" }, + { url = "https://files.pythonhosted.org/packages/dc/2e/a44c20c58aeed0355f2d326969a181696aeb551a25195f47563908a815be/jiter-0.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5419d4aa2024961da9fe12a9cfe7484996735dca99e8e090b5c88595ef1951ff", size = 191338, upload-time = "2026-04-10T14:28:02.853Z" }, + { url = "https://files.pythonhosted.org/packages/32/a1/ef34ca2cab2962598591636a1804b93645821201cc0095d4a93a9a329c9d/jiter-0.14.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a25ffa2dbbdf8721855612f6dca15c108224b12d0c4024d0ac3d7902132b4211", size = 311366, upload-time = "2026-04-10T14:28:27.943Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/520576a532a6b8a6f42747afed289c8448c879a34d7802fe2c832d4fd38f/jiter-0.14.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ac9cbaa86c10996b92bd12c91659b60f939f8e28fcfa6bc11a0e90a774ce95b", size = 309873, upload-time = "2026-04-10T14:28:29.688Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7c/c16db114ea1f2f532f198aa8dc39585026af45af362c69a0492f31bc4821/jiter-0.14.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:844e73b6c56b505e9e169234ea3bdea2ea43f769f847f47ac559ba1d2361ebea", size = 344816, upload-time = "2026-04-10T14:28:31.348Z" }, + { url = "https://files.pythonhosted.org/packages/99/8f/15e7741ff19e9bcd4d753f7ff22f988fd54592f134ca13701c13ea8c20e0/jiter-0.14.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e52c076f187405fc21523c746c04399c9af8ece566077ed147b2126f2bcba577", size = 351445, upload-time = "2026-04-10T14:28:33.093Z" }, + { url = "https://files.pythonhosted.org/packages/21/42/9042c3f3019de4adcb8c16591c325ec7255beea9fcd33a42a43f3b0b1000/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:fbd9e482663ca9d005d051330e4d2d8150bb208a209409c10f7e7dfdf7c49da9", size = 308810, upload-time = "2026-04-10T14:28:34.673Z" }, + { url = "https://files.pythonhosted.org/packages/60/cf/a7e19b308bd86bb04776803b1f01a5f9a287a4c55205f4708827ee487fbf/jiter-0.14.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:33a20d838b91ef376b3a56896d5b04e725c7df5bc4864cc6569cf046a8d73b6d", size = 308443, upload-time = "2026-04-10T14:28:36.658Z" }, + { url = "https://files.pythonhosted.org/packages/ca/44/e26ede3f0caeff93f222559cb0cc4ca68579f07d009d7b6010c5b586f9b1/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:432c4db5255d86a259efde91e55cb4c8d18c0521d844c9e2e7efcce3899fb016", size = 343039, upload-time = "2026-04-10T14:28:38.356Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/1f9ada30cef7b05e74bb06f52127e7a724976c225f46adb65c37b1dadfb6/jiter-0.14.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f00d94b281174144d6532a04b66a12cb866cbdc47c3af3bfe2973677f9861a", size = 349613, upload-time = "2026-04-10T14:28:40.066Z" }, +] + +[[package]] +name = "jsonpatch" +version = "1.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpointer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, +] + +[[package]] +name = "langchain" +version = "1.2.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/4e/b651ecac63af474b28519384f7011294493d139937b1a9591581291eef34/langchain-1.2.18.tar.gz", hash = "sha256:7e829dbf117affadfd2067a0e97b4af20222f535f30fb812a28472d842c1074c", size = 582882, upload-time = "2026-05-08T13:59:19.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/20/959f6098c79158afe5aedce7de05c3700f10d293890ef9e5dace6c3ad94b/langchain-1.2.18-py3-none-any.whl", hash = "sha256:8432d43a65540845ed6f1a783d38d869c4659a6b9405f9a510169ad40d2f7bae", size = 113643, upload-time = "2026-05-08T13:59:18.461Z" }, +] + +[[package]] +name = "langchain-acp" +source = { editable = "packages/adapters/langchain-acp" } +dependencies = [ + { name = "agent-client-protocol" }, + { name = "langchain" }, + { name = "langgraph" }, + { name = "pydantic" }, + { name = "typing-extensions" }, +] + +[package.optional-dependencies] +deepagents = [ + { name = "deepagents" }, +] + +[package.metadata] +requires-dist = [ + { name = "agent-client-protocol", specifier = ">=0.9.0" }, + { name = "deepagents", marker = "extra == 'deepagents'", specifier = ">=0.5.0" }, + { name = "langchain", specifier = ">=1.0.0" }, + { name = "langgraph", specifier = ">=1.0.0" }, + { name = "pydantic", specifier = ">=2.7" }, + { name = "typing-extensions", specifier = ">=4.12.0" }, +] +provides-extras = ["deepagents"] + +[[package]] +name = "langchain-anthropic" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anthropic" }, + { name = "langchain-core" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/e3/d2f9dec95602524b1cfb4be2747ba5bc38d32501b2a56cb4bcb76e80bb45/langchain_anthropic-1.4.3.tar.gz", hash = "sha256:f8a2442463c0629b1b3110eaeaa56fdbdc87df2a802f8c7f5ecf611eb4874ec8", size = 685219, upload-time = "2026-05-03T17:33:27.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/55/482a1968c95275e8be6d8c1e53b54f0f7be0b8b155ce1608c947a95cf543/langchain_anthropic-1.4.3-py3-none-any.whl", hash = "sha256:65466e0f2f95909a009708f2958e917dfdbfab79c612b4484a30866a85e1f291", size = 50389, upload-time = "2026-05-03T17:33:25.671Z" }, +] + +[[package]] +name = "langchain-core" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpatch" }, + { name = "langchain-protocol" }, + { name = "langsmith" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "uuid-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/ae/8b74458fc3850ec3d150eb9f45e857db129dafa801fb5cf173dfc9f8bbf3/langchain_core-1.3.3.tar.gz", hash = "sha256:fa510a5db8efdc0c6ff41c0939fb5c00a0183c11f6b84233e892e3227ff69182", size = 915041, upload-time = "2026-05-05T19:02:36.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/01/4771b7ab2af1d1aba5b710bd8f13d9225c609425214b357590a17b01be77/langchain_core-1.3.3-py3-none-any.whl", hash = "sha256:18aae8506f37da7f74398492279a7d6efcee4f8e23c4c41c7af080eeb7ef7bd1", size = 543857, upload-time = "2026-05-05T19:02:34.52Z" }, +] + +[[package]] +name = "langchain-google-genai" +version = "4.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filetype" }, + { name = "google-genai" }, + { name = "langchain-core" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/78/dfe068937338727b0dee637d971d59fe2fa275f9d0f0edee3fa80e811846/langchain_google_genai-4.2.2.tar.gz", hash = "sha256:5fc774bf41d1dc1c1a5ba8d7b9f2017dfa77e30653c9b44d2dfbaf0e877e7388", size = 267457, upload-time = "2026-04-15T15:08:32.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/5c/adf81d68ab89b4cf505e690f8c1956d11b5969c831c951c7b4b1b1818080/langchain_google_genai-4.2.2-py3-none-any.whl", hash = "sha256:c8d09aac0304d26f1c2483e41a350f15587af1fbe034c39a304e1e17a3b743f3", size = 67605, upload-time = "2026-04-15T15:08:31.346Z" }, +] + +[[package]] +name = "langchain-openai" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "openai" }, + { name = "tiktoken" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/0e/d8e16c28aa67106d285e63b8ffc04c5af68341e345ce24a0751dbf2e167e/langchain_openai-1.2.1.tar.gz", hash = "sha256:ee4480b787706361b7125fad46930589a624df87aa158c6986ef1fad10d10675", size = 1146092, upload-time = "2026-04-24T19:46:43.328Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/55/2865b18ee3a3dd11160b8c4b2cf37e75bf2a4a8d1d38868ffffc7b7cc180/langchain_openai-1.2.1-py3-none-any.whl", hash = "sha256:a80732185030d4f453dda6c25feef46f645f665423fdffe38ae3edf1ac3c6c4d", size = 98626, upload-time = "2026-04-24T19:46:41.971Z" }, +] + +[[package]] +name = "langchain-protocol" +version = "0.0.15" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/24/9777489d6fbbee64af0c8f96d4f840239c408cf694f3394672807dafc490/langchain_protocol-0.0.15.tar.gz", hash = "sha256:9ab2d11ee73944754f10e037e717098d3a6796f0e58afa9cadda6154e7655ade", size = 5862, upload-time = "2026-05-01T22:30:04.748Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/7a/9c97a7b9cbe4c5dc6a44cdb1545450c28f0c8ce89b9c1f0ee7fbad896263/langchain_protocol-0.0.15-py3-none-any.whl", hash = "sha256:461eb794358f83d5e42635a5797799ffec7b4702314e34edf73ac21e75d3ef79", size = 6982, upload-time = "2026-05-01T22:30:03.877Z" }, +] + +[[package]] +name = "langgraph" +version = "1.1.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, + { name = "langgraph-prebuilt" }, + { name = "langgraph-sdk" }, + { name = "pydantic" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/b3/7dec224369c7938eb3227ff69542a0d0f517862a0d27945b8c395f2a781f/langgraph-1.1.10.tar.gz", hash = "sha256:3115beb58203283c98d8752a90c034f3432177d2979a1fe205f76e5f1b744500", size = 560685, upload-time = "2026-04-27T17:19:10.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/07/057dc1aa7991115fca53f1fa6573a7cc0dd296c05360c672cc67fdb6245b/langgraph-1.1.10-py3-none-any.whl", hash = "sha256:8a4f163f72f4401648d0c11b48ee906947d938ba8cf1f474540fe591534f0d17", size = 173750, upload-time = "2026-04-27T17:19:09.073Z" }, +] + +[[package]] +name = "langgraph-checkpoint" +version = "4.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "ormsgpack" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/e1/885e49cdafceb4c74dae4573bc5dd6054c6c640382ee73104532f33dca46/langgraph_checkpoint-4.0.3.tar.gz", hash = "sha256:a7b5e2ca18fb79b55edf19396d4ee446f8a53dcb7a4ec62ce6f1c7e00bb5af7f", size = 174009, upload-time = "2026-04-27T14:34:02.777Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/ee/ecd3fa2e893746dde3b768daca2a4935208bc77d09445437ccfffb4a8c9b/langgraph_checkpoint-4.0.3-py3-none-any.whl", hash = "sha256:b91b765712a2311a5b198760f714b7ab9b376d01c047ed78d9b9a3e80df802a3", size = 51682, upload-time = "2026-04-27T14:34:01.51Z" }, +] + +[[package]] +name = "langgraph-prebuilt" +version = "1.0.13" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/a4/f8ac75fa7c503103f0cf7680944e28bbaaef74c19a8d163d7346869cc369/langgraph_prebuilt-1.0.13.tar.gz", hash = "sha256:ad219782a80e1718e7e7794de49e0ae307111d45cbcffab9a52725a66a609456", size = 172913, upload-time = "2026-04-30T01:48:15.742Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/ef/5ada0bef4013ef5ae53a0ca1de5736517f1076a54d313f156ca545ec65d5/langgraph_prebuilt-1.0.13-py3-none-any.whl", hash = "sha256:7055e9fad41fbd3593800aed0aea0a6e974b17f33ed51b80d3d3a031212dd7c0", size = 37214, upload-time = "2026-04-30T01:48:14.507Z" }, +] + +[[package]] +name = "langgraph-sdk" +version = "0.3.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "orjson" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/f1/134046c20bc4a4a15d410d1d21c9e298a3e9923777b4cc867b8669bc636b/langgraph_sdk-0.3.14.tar.gz", hash = "sha256:acd1674c538e97f3cdaa610f6dd7e34bc9bad30167f0ccc482dcd563325e81f5", size = 198162, upload-time = "2026-05-05T18:40:03.524Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/96/1c9f9fbfe756ddd850a2585e7f1949d8ebb97fdaa7a5eff8f45ed1314670/langgraph_sdk-0.3.14-py3-none-any.whl", hash = "sha256:68935bf6f4924eda92617a9e5dfb4f4281197508c648cb9d62ff083907607f9d", size = 97028, upload-time = "2026-05-05T18:40:02.099Z" }, +] + +[[package]] +name = "langsmith" +version = "0.8.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "uuid-utils" }, + { name = "xxhash" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/8a/1e8ea5e8bab2a65fa95bd36229ef38e8723ec46e430e20ca2d953487a7f1/langsmith-0.8.3.tar.gz", hash = "sha256:767ff7a8d136ed42926bf99059ac631dc6883542d6e3104b32e71c7625e1fa05", size = 4460330, upload-time = "2026-05-07T19:56:56.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/a9/51e644c1f1dbc3dd7d22dfd6412eab206d538c81e024e4f287373544bdcb/langsmith-0.8.3-py3-none-any.whl", hash = "sha256:b2e40e308222fa0beb2dccee3b4b30bfee9062d7a4f20a3e3e93df3c51a08ab4", size = 399048, upload-time = "2026-05-07T19:56:53.994Z" }, +] + +[[package]] +name = "logfire-api" +version = "4.32.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/1b/0c74ad85f977743ba4c589e46e0cb138d6a6e69487830f4e86ebbdb145a3/logfire_api-4.32.1.tar.gz", hash = "sha256:5e8714b2bb5fb5d1f4a4a833941e4ca711b75d2c1f98e76c5ad680fe6991af6a", size = 78788, upload-time = "2026-04-15T14:11:58.788Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/ab/d5adeab6253c7ecd5904fc5ef3265859f218610caf4e1e55efe9aff6ac49/logfire_api-4.32.1-py3-none-any.whl", hash = "sha256:4b4c27cf6e27e8e26ef4b22a77f2a2988dd1d07e2d24ee70673ef34b234fb8a5", size = 124394, upload-time = "2026-04-15T14:11:56.157Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/29/6d2bcf41ae40802c4beda2432396fff97b8456fb496371d1bc7aad6512ec/mkdocs_material-9.7.6.tar.gz", hash = "sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69", size = 4097959, upload-time = "2026-03-19T15:41:58.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl", hash = "sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba", size = 9305470, upload-time = "2026-03-19T15:41:55.217Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/5d/f888d4d3eb31359b327bc9b17a212d6ef03fe0b0682fbb3fc2cb849fb12b/mkdocstrings-1.0.4.tar.gz", hash = "sha256:3969a6515b77db65fd097b53c1b7aa4ae840bd71a2ee62a6a3e89503446d7172", size = 100088, upload-time = "2026-04-15T09:16:53.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl", hash = "sha256:63464b4b29053514f32a1dbbf604e52876d5e638111b0c295ab7ed3cac73ca9b", size = 35560, upload-time = "2026-04-15T09:16:51.436Z" }, +] + +[package.optional-dependencies] +python = [ + { name = "mkdocstrings-python" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffelib" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/33/c225eaf898634bdda489a6766fc35d1683c640bffe0e0acd10646b13536d/mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8", size = 199083, upload-time = "2026-02-20T10:38:36.368Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", size = 104779, upload-time = "2026-02-20T10:38:34.517Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "nodejs-wheel-binaries" +version = "24.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/70/a1e4f4d5986768ab90cc860b1cc3660fd2ded74ca175a900a5c29f839c7d/nodejs_wheel_binaries-24.15.0.tar.gz", hash = "sha256:b43f5c4f6e5768d8845b2ae4682eb703a19bf7aadc84187e2d903ed3a611c859", size = 8057, upload-time = "2026-04-19T15:48:16.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/66/54051d14853d6ab4fb85f8be9b042b530be653357fb9a19557498bc91ab7/nodejs_wheel_binaries-24.15.0-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:a6232fa8b754220941f52388c8ead923f7c1c7fdf0ea0d98f657523bd9a81ef4", size = 55173485, upload-time = "2026-04-19T15:47:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5f/66acada164da5ca10a0824db021aa7394ae18396c550cd9280e839a43126/nodejs_wheel_binaries-24.15.0-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:001a6b62c69d9109c1738163cca00608dd2722e8663af59300054ea02610972d", size = 55348100, upload-time = "2026-04-19T15:47:40.521Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2d/0cbd5ff40c9bb030ca1735d8f8793bd74f08a4cbd49100a1d19313ea57ab/nodejs_wheel_binaries-24.15.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:0fbc48765e60ed0ff30d43898dbf5cadbadf2e5f1e7f204afc2b01493b7ebce6", size = 59668206, upload-time = "2026-04-19T15:47:46.848Z" }, + { url = "https://files.pythonhosted.org/packages/da/d5/91ac63951ec75927a486b83b8cafe650e360fa70ac01dc94adfb32b93b97/nodejs_wheel_binaries-24.15.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:20ee0536809795da8a4942fc1ab4cbdebbcaaf29383eab67ba8874268fb00008", size = 60206736, upload-time = "2026-04-19T15:47:52.668Z" }, + { url = "https://files.pythonhosted.org/packages/db/72/dc22776974d928869c0c30d23ee98ed7df254243c2df68f09f5963e8e8b8/nodejs_wheel_binaries-24.15.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1fade6c214285e72472ca40a631e98ff36559671cd5eefc8bf009471d67f04b4", size = 61720456, upload-time = "2026-04-19T15:47:58.325Z" }, + { url = "https://files.pythonhosted.org/packages/01/0a/34461b9050cb45ee371dccdefc622aef6351506ea2691b08fc761ca67150/nodejs_wheel_binaries-24.15.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3984cb8d87766567aee67a49743227ab40ede6f47734ec990ff90e50b74e7740", size = 62326172, upload-time = "2026-04-19T15:48:04.094Z" }, + { url = "https://files.pythonhosted.org/packages/c9/17/09252bf35672dba926649d59dfe51443a0f6955ad13784e91131d5ec82a2/nodejs_wheel_binaries-24.15.0-py2.py3-none-win_amd64.whl", hash = "sha256:a437601956b532dcb3082046e6978e622733f90edc0932cbb9adb3bb97a16501", size = 41543461, upload-time = "2026-04-19T15:48:09.332Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7e/b649777d148e1e0c2ce349156603cdb12f7ed99921b95d93717393650193/nodejs_wheel_binaries-24.15.0-py2.py3-none-win_arm64.whl", hash = "sha256:bdf4a431e08321a32efc604111c6f23941f87055d796a537e8c4110daecad23f", size = 39233248, upload-time = "2026-04-19T15:48:13.326Z" }, +] + +[[package]] +name = "openai" +version = "2.36.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/a1/4d5e84cf51720fc1526cc49e10ac1961abcccb55b0efb3d970db1e9a2728/openai-2.36.0.tar.gz", hash = "sha256:139dea0edd2f1b30c33d46ae1a6929e03906254140318e4608e98fe8c566f2e7", size = 753003, upload-time = "2026-05-07T17:33:17.075Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/1c/5d43735b2553baae2a5e899dcbcd0670a86930d993184d72ca909bf11c9b/openai-2.36.0-py3-none-any.whl", hash = "sha256:143f6194b548dbc2c921af1f1b03b9f14c85fed8a75b5b516f5bcc11a2a50c63", size = 1302361, upload-time = "2026-05-07T17:33:15.063Z" }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.41.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fa/fc/b7564cbef36601aef0d6c9bc01f7badb64be8e862c2e1c3c5c3b43b53e4f/opentelemetry_api-1.41.1.tar.gz", hash = "sha256:0ad1814d73b875f84494387dae86ce0b12c68556331ce6ce8fe789197c949621", size = 71416, upload-time = "2026-04-24T13:15:38.262Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/59/3e7118ed140f76b0982ba4321bdaed1997a0473f9720de2d10788a577033/opentelemetry_api-1.41.1-py3-none-any.whl", hash = "sha256:a22df900e75c76dc08440710e51f52f1aa6b451b429298896023e60db5b3139f", size = 69007, upload-time = "2026-04-24T13:15:15.662Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163, upload-time = "2026-05-06T15:11:08.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/51/3fb9e65ae76ee97bd611869a503fa3fc0a6e81dd8b737cf3003f682df7ff/orjson-3.11.9-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f", size = 228522, upload-time = "2026-05-06T15:09:35.362Z" }, + { url = "https://files.pythonhosted.org/packages/16/fa/9d54b07cb3f3b0bfd57841478e42d7a0ece4a9f49f9907eecf5a45461687/orjson-3.11.9-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c", size = 128463, upload-time = "2026-05-06T15:09:37.063Z" }, + { url = "https://files.pythonhosted.org/packages/88/b1/6ceafc2eefd0a553e3be77ce6c49d107e772485d9568629376171c50e634/orjson-3.11.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5", size = 132306, upload-time = "2026-05-06T15:09:38.299Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/f11311285324a40aab1e3031385c50b635a7cd0734fdaf60c7e89a696f60/orjson-3.11.9-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c", size = 127988, upload-time = "2026-05-06T15:09:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/9e/85/0ef63bcf1337f44031ce9b91b1919563f62a37527b3ea4368bb15a22e5d7/orjson-3.11.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:277fefe9d76ee17eb14debf399e3533d4d63b5f677a4d3719eb763536af1f4bd", size = 135188, upload-time = "2026-05-06T15:09:40.957Z" }, + { url = "https://files.pythonhosted.org/packages/05/94/b0d27090ea8a2095db3c2bd1b1c96f96f19bbb494d7fef33130e846e613d/orjson-3.11.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62", size = 145937, upload-time = "2026-05-06T15:09:42.249Z" }, + { url = "https://files.pythonhosted.org/packages/09/eb/75d50c29c05b8054013e221e598820a365c8e64065312e75e202ed880709/orjson-3.11.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877", size = 132758, upload-time = "2026-05-06T15:09:43.945Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/360686f39348aa88827cb6fbf7dc606fd41c831a35235e1abf1db8e3a9e6/orjson-3.11.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1", size = 133971, upload-time = "2026-05-06T15:09:45.239Z" }, + { url = "https://files.pythonhosted.org/packages/0e/30/3178eb16f3221aeef068b6f1f1ebe05f656ea5c6dffe9f6c917329fe17a3/orjson-3.11.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd", size = 141685, upload-time = "2026-05-06T15:09:46.858Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f1/ff2f19ed0225f9680fafa42febca3570dd59444ebf190980738d376214c2/orjson-3.11.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff", size = 415167, upload-time = "2026-05-06T15:09:48.312Z" }, + { url = "https://files.pythonhosted.org/packages/9b/61/863bddf0da6e9e586765414debd54b4e58db05f560902b6d00658cb88636/orjson-3.11.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:16969c9d369c98eb084889c6e4d2d39b77c7eb38ceccf8da2a9fff62ae908980", size = 147913, upload-time = "2026-05-06T15:09:49.733Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4081492586d75b073d60c5271a8d0f05a0955cabf1e34c8473f6fcd84235/orjson-3.11.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2", size = 136959, upload-time = "2026-05-06T15:09:51.311Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bd/70b6ab193594d7abb875320c0a7c8335e846f28968c432c31042409c3c8d/orjson-3.11.9-cp311-cp311-win32.whl", hash = "sha256:14ed654580c1ed2bc217352ec82f91b047aef82951aa71c7f64e0dcb03c0e180", size = 131533, upload-time = "2026-05-06T15:09:52.637Z" }, + { url = "https://files.pythonhosted.org/packages/3f/17/1a1a228183d62d1b77e2c30d210f47dd4768b310ebe1607c63e3c0e3a71e/orjson-3.11.9-cp311-cp311-win_amd64.whl", hash = "sha256:57ea77fb70a448ce87d18fca050193202a3da5e54598f6501ca5476fb66cfe02", size = 127106, upload-time = "2026-05-06T15:09:54.204Z" }, + { url = "https://files.pythonhosted.org/packages/b8/95/285de5fa296d09681ee9c546cd4a8aeb773b701cf343dc125994f4d52953/orjson-3.11.9-cp311-cp311-win_arm64.whl", hash = "sha256:19b72ed11572a2ee51a67a903afbe5af504f84ed6f529c0fe44b0ab3fb5cc697", size = 126848, upload-time = "2026-05-06T15:09:55.551Z" }, + { url = "https://files.pythonhosted.org/packages/16/6d/11867a3ffa3a3608d84a4de51ef4dd0896d6b5cc9132fbe1daf593e677bc/orjson-3.11.9-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49", size = 228515, upload-time = "2026-05-06T15:09:57.265Z" }, + { url = "https://files.pythonhosted.org/packages/24/75/05912954c8b288f34fcf5cd4b9b071cb4f6e77b9961e175e56ebb258089f/orjson-3.11.9-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291", size = 128409, upload-time = "2026-05-06T15:09:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/ab/86/1c3a47df3bc8191ea9ac51603bbb872a95167a364320c269f2557911f406/orjson-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09", size = 132106, upload-time = "2026-05-06T15:10:00.798Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cf/b33b5f3e695ae7d63feef9d915c37cc3b8f465493dcd4f8e0b4c697a2366/orjson-3.11.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4", size = 127864, upload-time = "2026-05-06T15:10:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/31/6a/6cf69385a58208024fcb8c014e2141b8ce838aba6492b589f8acfff97fab/orjson-3.11.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882", size = 135213, upload-time = "2026-05-06T15:10:03.515Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f8/0b1bd3e8f2efcdd376af5c8cfd79eaf13f018080c0089c80ebd724e3c7fb/orjson-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff", size = 145994, upload-time = "2026-05-06T15:10:05.083Z" }, + { url = "https://files.pythonhosted.org/packages/f3/59/dab79f61044c529d2c81aecdc589b1f833a1c8dec11ba3b1c2498a02ca7e/orjson-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe", size = 132744, upload-time = "2026-05-06T15:10:06.853Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a4/82b7a2fe5d8a67a59ed831b24d59a3d46ea7d207b66e1602d376541d94a6/orjson-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61", size = 134014, upload-time = "2026-05-06T15:10:08.213Z" }, + { url = "https://files.pythonhosted.org/packages/50/c7/375e83a76851b73b2e39f3bcf0e5a19e2b89bad13e5bca97d0b293d27f24/orjson-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2", size = 141509, upload-time = "2026-05-06T15:10:09.595Z" }, + { url = "https://files.pythonhosted.org/packages/7f/7c/49d5d82a3d3097f641f094f552131f1e2723b0b8cb0fa2874ab65ecfffa6/orjson-3.11.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206", size = 415127, upload-time = "2026-05-06T15:10:11.049Z" }, + { url = "https://files.pythonhosted.org/packages/3a/dc/7446c538590d55f455647e5f3c61fc33f7108714e7afcffa6a2a033f8350/orjson-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f", size = 148025, upload-time = "2026-05-06T15:10:12.842Z" }, + { url = "https://files.pythonhosted.org/packages/df/e5/4d2d8af06f788329b4f78f8cc3679bb395392fcaa1e4d8d3c33e85308fa4/orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa", size = 136943, upload-time = "2026-05-06T15:10:14.405Z" }, + { url = "https://files.pythonhosted.org/packages/06/69/850264ccf6d80f6b174620d30a87f65c9b1490aba33fe6b62798e618cad3/orjson-3.11.9-cp312-cp312-win32.whl", hash = "sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470", size = 131606, upload-time = "2026-05-06T15:10:15.791Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/973a43fc9c55e20f2051e9830997649f669be0cb3ca52192087c0143f118/orjson-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be", size = 127101, upload-time = "2026-05-06T15:10:17.129Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/495470f0e4a18f73fa10b7f6b84b464ec4cc5291c4e0c7c2a6c400bef006/orjson-3.11.9-cp312-cp312-win_arm64.whl", hash = "sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624", size = 126736, upload-time = "2026-05-06T15:10:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/93fcc25907235c344ae73122f8a4e01d2d393ef062b4af7d2e2487a32c37/orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021", size = 228458, upload-time = "2026-05-06T15:10:20.079Z" }, + { url = "https://files.pythonhosted.org/packages/8f/27/b1e6dadb3c080313c03fdd8067b85e6a0460c7d8d6a1c3984ef77b904e4d/orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c", size = 128368, upload-time = "2026-05-06T15:10:21.549Z" }, + { url = "https://files.pythonhosted.org/packages/21/0f/c9ede0bf052f6b4051e64a7d4fa91b725cccf8321a6a786e86eb03519f00/orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81", size = 132070, upload-time = "2026-05-06T15:10:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/fd/26/d398e28048dc18205bbe812f2c88cb9b40313db2470778e25964796458fe/orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a", size = 127892, upload-time = "2026-05-06T15:10:24.714Z" }, + { url = "https://files.pythonhosted.org/packages/66/60/52b0054c4c700d5aa7fc5b7ca96917400d8f061307778578e67a10e25852/orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10", size = 135217, upload-time = "2026-05-06T15:10:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/d5/97/1e3dc2b2a28b7b2528f403d2fc1d79ec5f39af3bc143ab65d3ec26426385/orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362", size = 145980, upload-time = "2026-05-06T15:10:28.062Z" }, + { url = "https://files.pythonhosted.org/packages/fc/39/31fbfe7850f2de32dee7e7e5c09f26d403ab01e440ac96001c6b01ad3c99/orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97", size = 132738, upload-time = "2026-05-06T15:10:29.727Z" }, + { url = "https://files.pythonhosted.org/packages/a1/08/dca0082dd2a194acb93e5457e73455388e2e2ca464a2672449a9ddbb679d/orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218", size = 134033, upload-time = "2026-05-06T15:10:31.152Z" }, + { url = "https://files.pythonhosted.org/packages/11/d4/5bdb0626801230139987385554c5d4c42255218ac906525bf4347f22cd95/orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9", size = 141492, upload-time = "2026-05-06T15:10:32.641Z" }, + { url = "https://files.pythonhosted.org/packages/fa/88/a21fb53b3ede6703aede6dce4710ed4111e5b201cfa6bbff5e544f9d47d7/orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677", size = 415087, upload-time = "2026-05-06T15:10:34.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/57/1b30daf70f0d8180e9a73cefbfbdd99e4bf19eb020466502b01fba7e0e50/orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4", size = 148031, upload-time = "2026-05-06T15:10:36.358Z" }, + { url = "https://files.pythonhosted.org/packages/04/83/45fbb6d962e260807f99441db9613cee868ceda4baceda59b3720a563f97/orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0", size = 136915, upload-time = "2026-05-06T15:10:38.013Z" }, + { url = "https://files.pythonhosted.org/packages/5f/cc/2d10025f9056d376e4127ec05a5808b218d46f035fdc08178a5411b34250/orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32", size = 131613, upload-time = "2026-05-06T15:10:39.569Z" }, + { url = "https://files.pythonhosted.org/packages/67/bd/2775ff28bfe883b9aa1ff348300542eb2ef1ee18d8ae0e3a49846817a865/orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979", size = 127086, upload-time = "2026-05-06T15:10:41.262Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/d26799e580939e32a7da9a39531bc9e58e15ca32ffaa6a8cb3e9bb0d22cd/orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254", size = 126696, upload-time = "2026-05-06T15:10:42.651Z" }, + { url = "https://files.pythonhosted.org/packages/8e/eb/5da01e356015aee6ecfa1187ced87aef51364e306f5e695dd52719bf0e78/orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e", size = 228465, upload-time = "2026-05-06T15:10:44.097Z" }, + { url = "https://files.pythonhosted.org/packages/64/62/3e0e0c14c957133bcd855395c62b55ed4e3b0af23ffea11b032cb1dcbdb1/orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e", size = 128364, upload-time = "2026-05-06T15:10:45.839Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5a/07d8aa117211a8ed7630bda80c8c0b14d04e0f8dcf99bcf49656e4a710eb/orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0", size = 132063, upload-time = "2026-05-06T15:10:47.267Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ec/4acaf21483e18aa945be74a474c74b434f284b549f275a0a39b9f98956e9/orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124", size = 122356, upload-time = "2026-05-06T15:10:48.765Z" }, + { url = "https://files.pythonhosted.org/packages/13/d8/5f0555e7638801323b7a75850f92e7dfa891bc84fe27a1ba4449170d1200/orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c", size = 129592, upload-time = "2026-05-06T15:10:50.13Z" }, + { url = "https://files.pythonhosted.org/packages/b6/30/ed9860412a3603ceb3c5955bfd72d28b9d0e7ba6ed81add14f83d7114236/orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7", size = 140491, upload-time = "2026-05-06T15:10:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/d0/17/adc514dea7ac7c505527febf884934b815d34f0c7b8693c1a8b39c5c4a57/orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1", size = 127309, upload-time = "2026-05-06T15:10:53.329Z" }, + { url = "https://files.pythonhosted.org/packages/76/3e/c0b690253f0b82d86e99949af13533363acfb5432ecb5d53dd5b3bce9c34/orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db", size = 134030, upload-time = "2026-05-06T15:10:54.988Z" }, + { url = "https://files.pythonhosted.org/packages/c1/7a/bc82a0bb25e9faaf92dc4d9ef002732efc09737706af83e346788641d4a7/orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b", size = 141482, upload-time = "2026-05-06T15:10:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/01/55/e69188b939f77d5d32a9833745ace31ea5ccae3ab613a1ec185d3cd2c4fb/orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972", size = 415178, upload-time = "2026-05-06T15:10:58.446Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/b8a5a7ac527e80b9cb11d51e3f6689b709279183264b9ec5c7bc680bb8b5/orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0", size = 148089, upload-time = "2026-05-06T15:11:00.441Z" }, + { url = "https://files.pythonhosted.org/packages/97/4e/00503f64204bf859b37213a63927028f30fb6268cd8677fb0a5ad48155e1/orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586", size = 136921, upload-time = "2026-05-06T15:11:02.176Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ba/a23b82a0a8d0ed7bed4e5f5035aae751cad4ff6a1e8d2ecd14d8860f5929/orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673", size = 131638, upload-time = "2026-05-06T15:11:03.696Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/0c6798456bade745c75c452342dabacce5798196483e77e643be1f53877d/orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b", size = 127078, upload-time = "2026-05-06T15:11:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/16/21/5a3f1e8913103b703a436a5664238e5b965ec392b555fe68943ea3691e6b/orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9", size = 126687, upload-time = "2026-05-06T15:11:06.602Z" }, +] + +[[package]] +name = "ormsgpack" +version = "1.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/0c/f1761e21486942ab9bb6feaebc610fa074f7c5e496e6962dea5873348077/ormsgpack-1.12.2.tar.gz", hash = "sha256:944a2233640273bee67521795a73cf1e959538e0dfb7ac635505010455e53b33", size = 39031, upload-time = "2026-01-18T20:55:28.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/08/8b68f24b18e69d92238aa8f258218e6dfeacf4381d9d07ab8df303f524a9/ormsgpack-1.12.2-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bd5f4bf04c37888e864f08e740c5a573c4017f6fd6e99fa944c5c935fabf2dd9", size = 378266, upload-time = "2026-01-18T20:55:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/29fc13044ecb7c153523ae0a1972269fcd613650d1fa1a9cec1044c6b666/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34d5b28b3570e9fed9a5a76528fc7230c3c76333bc214798958e58e9b79cc18a", size = 203035, upload-time = "2026-01-18T20:55:30.59Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c2/00169fb25dd8f9213f5e8a549dfb73e4d592009ebc85fbbcd3e1dcac575b/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3708693412c28f3538fb5a65da93787b6bbab3484f6bc6e935bfb77a62400ae5", size = 210539, upload-time = "2026-01-18T20:55:48.569Z" }, + { url = "https://files.pythonhosted.org/packages/1b/33/543627f323ff3c73091f51d6a20db28a1a33531af30873ea90c5ac95a9b5/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43013a3f3e2e902e1d05e72c0f1aeb5bedbb8e09240b51e26792a3c89267e181", size = 212401, upload-time = "2026-01-18T20:56:10.101Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5d/f70e2c3da414f46186659d24745483757bcc9adccb481a6eb93e2b729301/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7c8b1667a72cbba74f0ae7ecf3105a5e01304620ed14528b2cb4320679d2869b", size = 387082, upload-time = "2026-01-18T20:56:12.047Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d6/06e8dc920c7903e051f30934d874d4afccc9bb1c09dcaf0bc03a7de4b343/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:df6961442140193e517303d0b5d7bc2e20e69a879c2d774316125350c4a76b92", size = 482346, upload-time = "2026-01-18T20:56:05.152Z" }, + { url = "https://files.pythonhosted.org/packages/66/c4/f337ac0905eed9c393ef990c54565cd33644918e0a8031fe48c098c71dbf/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6a4c34ddef109647c769d69be65fa1de7a6022b02ad45546a69b3216573eb4a", size = 425181, upload-time = "2026-01-18T20:55:37.83Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/6d5758fabef3babdf4bbbc453738cc7de9cd3334e4c38dd5737e27b85653/ormsgpack-1.12.2-cp311-cp311-win_amd64.whl", hash = "sha256:73670ed0375ecc303858e3613f407628dd1fca18fe6ac57b7b7ce66cc7bb006c", size = 117182, upload-time = "2026-01-18T20:55:31.472Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/17a15549233c37e7fd054c48fe9207492e06b026dbd872b826a0b5f833b6/ormsgpack-1.12.2-cp311-cp311-win_arm64.whl", hash = "sha256:c2be829954434e33601ae5da328cccce3266b098927ca7a30246a0baec2ce7bd", size = 111464, upload-time = "2026-01-18T20:55:38.811Z" }, + { url = "https://files.pythonhosted.org/packages/4c/36/16c4b1921c308a92cef3bf6663226ae283395aa0ff6e154f925c32e91ff5/ormsgpack-1.12.2-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7a29d09b64b9694b588ff2f80e9826bdceb3a2b91523c5beae1fab27d5c940e7", size = 378618, upload-time = "2026-01-18T20:55:50.835Z" }, + { url = "https://files.pythonhosted.org/packages/c0/68/468de634079615abf66ed13bb5c34ff71da237213f29294363beeeca5306/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b39e629fd2e1c5b2f46f99778450b59454d1f901bc507963168985e79f09c5d", size = 203186, upload-time = "2026-01-18T20:56:11.163Z" }, + { url = "https://files.pythonhosted.org/packages/73/a9/d756e01961442688b7939bacd87ce13bfad7d26ce24f910f6028178b2cc8/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:958dcb270d30a7cb633a45ee62b9444433fa571a752d2ca484efdac07480876e", size = 210738, upload-time = "2026-01-18T20:56:09.181Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ba/795b1036888542c9113269a3f5690ab53dd2258c6fb17676ac4bd44fcf94/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d379d72b6c5e964851c77cfedfb386e474adee4fd39791c2c5d9efb53505cc", size = 212569, upload-time = "2026-01-18T20:56:06.135Z" }, + { url = "https://files.pythonhosted.org/packages/6c/aa/bff73c57497b9e0cba8837c7e4bcab584b1a6dbc91a5dd5526784a5030c8/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8463a3fc5f09832e67bdb0e2fda6d518dc4281b133166146a67f54c08496442e", size = 387166, upload-time = "2026-01-18T20:55:36.738Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cf/f8283cba44bcb7b14f97b6274d449db276b3a86589bdb363169b51bc12de/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:eddffb77eff0bad4e67547d67a130604e7e2dfbb7b0cde0796045be4090f35c6", size = 482498, upload-time = "2026-01-18T20:55:29.626Z" }, + { url = "https://files.pythonhosted.org/packages/05/be/71e37b852d723dfcbe952ad04178c030df60d6b78eba26bfd14c9a40575e/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcd55e5f6ba0dbce624942adf9f152062135f991a0126064889f68eb850de0dd", size = 425518, upload-time = "2026-01-18T20:55:49.556Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0c/9803aa883d18c7ef197213cd2cbf73ba76472a11fe100fb7dab2884edf48/ormsgpack-1.12.2-cp312-cp312-win_amd64.whl", hash = "sha256:d024b40828f1dde5654faebd0d824f9cc29ad46891f626272dd5bfd7af2333a4", size = 117462, upload-time = "2026-01-18T20:55:47.726Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9e/029e898298b2cc662f10d7a15652a53e3b525b1e7f07e21fef8536a09bb8/ormsgpack-1.12.2-cp312-cp312-win_arm64.whl", hash = "sha256:da538c542bac7d1c8f3f2a937863dba36f013108ce63e55745941dda4b75dbb6", size = 111559, upload-time = "2026-01-18T20:55:54.273Z" }, + { url = "https://files.pythonhosted.org/packages/eb/29/bb0eba3288c0449efbb013e9c6f58aea79cf5cb9ee1921f8865f04c1a9d7/ormsgpack-1.12.2-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5ea60cb5f210b1cfbad8c002948d73447508e629ec375acb82910e3efa8ff355", size = 378661, upload-time = "2026-01-18T20:55:57.765Z" }, + { url = "https://files.pythonhosted.org/packages/6e/31/5efa31346affdac489acade2926989e019e8ca98129658a183e3add7af5e/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3601f19afdbea273ed70b06495e5794606a8b690a568d6c996a90d7255e51c1", size = 203194, upload-time = "2026-01-18T20:56:08.252Z" }, + { url = "https://files.pythonhosted.org/packages/eb/56/d0087278beef833187e0167f8527235ebe6f6ffc2a143e9de12a98b1ce87/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29a9f17a3dac6054c0dce7925e0f4995c727f7c41859adf9b5572180f640d172", size = 210778, upload-time = "2026-01-18T20:55:17.694Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a2/072343e1413d9443e5a252a8eb591c2d5b1bffbe5e7bfc78c069361b92eb/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39c1bd2092880e413902910388be8715f70b9f15f20779d44e673033a6146f2d", size = 212592, upload-time = "2026-01-18T20:55:32.747Z" }, + { url = "https://files.pythonhosted.org/packages/a2/8b/a0da3b98a91d41187a63b02dda14267eefc2a74fcb43cc2701066cf1510e/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50b7249244382209877deedeee838aef1542f3d0fc28b8fe71ca9d7e1896a0d7", size = 387164, upload-time = "2026-01-18T20:55:40.853Z" }, + { url = "https://files.pythonhosted.org/packages/19/bb/6d226bc4cf9fc20d8eb1d976d027a3f7c3491e8f08289a2e76abe96a65f3/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5af04800d844451cf102a59c74a841324868d3f1625c296a06cc655c542a6685", size = 482516, upload-time = "2026-01-18T20:55:42.033Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f1/bb2c7223398543dedb3dbf8bb93aaa737b387de61c5feaad6f908841b782/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cec70477d4371cd524534cd16472d8b9cc187e0e3043a8790545a9a9b296c258", size = 425539, upload-time = "2026-01-18T20:55:24.727Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e8/0fb45f57a2ada1fed374f7494c8cd55e2f88ccd0ab0a669aa3468716bf5f/ormsgpack-1.12.2-cp313-cp313-win_amd64.whl", hash = "sha256:21f4276caca5c03a818041d637e4019bc84f9d6ca8baa5ea03e5cc8bf56140e9", size = 117459, upload-time = "2026-01-18T20:55:56.876Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d4/0cfeea1e960d550a131001a7f38a5132c7ae3ebde4c82af1f364ccc5d904/ormsgpack-1.12.2-cp313-cp313-win_arm64.whl", hash = "sha256:baca4b6773d20a82e36d6fd25f341064244f9f86a13dead95dd7d7f996f51709", size = 111577, upload-time = "2026-01-18T20:55:43.605Z" }, + { url = "https://files.pythonhosted.org/packages/94/16/24d18851334be09c25e87f74307c84950f18c324a4d3c0b41dabdbf19c29/ormsgpack-1.12.2-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bc68dd5915f4acf66ff2010ee47c8906dc1cf07399b16f4089f8c71733f6e36c", size = 378717, upload-time = "2026-01-18T20:55:26.164Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a2/88b9b56f83adae8032ac6a6fa7f080c65b3baf9b6b64fd3d37bd202991d4/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46d084427b4132553940070ad95107266656cb646ea9da4975f85cb1a6676553", size = 203183, upload-time = "2026-01-18T20:55:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/a9/80/43e4555963bf602e5bdc79cbc8debd8b6d5456c00d2504df9775e74b450b/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c010da16235806cf1d7bc4c96bf286bfa91c686853395a299b3ddb49499a3e13", size = 210814, upload-time = "2026-01-18T20:55:33.973Z" }, + { url = "https://files.pythonhosted.org/packages/78/e1/7cfbf28de8bca6efe7e525b329c31277d1b64ce08dcba723971c241a9d60/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18867233df592c997154ff942a6503df274b5ac1765215bceba7a231bea2745d", size = 212634, upload-time = "2026-01-18T20:55:28.634Z" }, + { url = "https://files.pythonhosted.org/packages/95/f8/30ae5716e88d792a4e879debee195653c26ddd3964c968594ddef0a3cc7e/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b009049086ddc6b8f80c76b3955df1aa22a5fbd7673c525cd63bf91f23122ede", size = 387139, upload-time = "2026-01-18T20:56:02.013Z" }, + { url = "https://files.pythonhosted.org/packages/dc/81/aee5b18a3e3a0e52f718b37ab4b8af6fae0d9d6a65103036a90c2a8ffb5d/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1dcc17d92b6390d4f18f937cf0b99054824a7815818012ddca925d6e01c2e49e", size = 482578, upload-time = "2026-01-18T20:55:35.117Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/71c9ba472d5d45f7546317f467a5fc941929cd68fb32796ca3d13dcbaec2/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f04b5e896d510b07c0ad733d7fce2d44b260c5e6c402d272128f8941984e4285", size = 425539, upload-time = "2026-01-18T20:56:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a6/ac99cd7fe77e822fed5250ff4b86fa66dd4238937dd178d2299f10b69816/ormsgpack-1.12.2-cp314-cp314-win_amd64.whl", hash = "sha256:ae3aba7eed4ca7cb79fd3436eddd29140f17ea254b91604aa1eb19bfcedb990f", size = 117493, upload-time = "2026-01-18T20:56:07.343Z" }, + { url = "https://files.pythonhosted.org/packages/3a/67/339872846a1ae4592535385a1c1f93614138566d7af094200c9c3b45d1e5/ormsgpack-1.12.2-cp314-cp314-win_arm64.whl", hash = "sha256:118576ea6006893aea811b17429bfc561b4778fad393f5f538c84af70b01260c", size = 111579, upload-time = "2026-01-18T20:55:21.161Z" }, + { url = "https://files.pythonhosted.org/packages/49/c2/6feb972dc87285ad381749d3882d8aecbde9f6ecf908dd717d33d66df095/ormsgpack-1.12.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7121b3d355d3858781dc40dafe25a32ff8a8242b9d80c692fd548a4b1f7fd3c8", size = 378721, upload-time = "2026-01-18T20:55:52.12Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9a/900a6b9b413e0f8a471cf07830f9cf65939af039a362204b36bd5b581d8b/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ee766d2e78251b7a63daf1cddfac36a73562d3ddef68cacfb41b2af64698033", size = 203170, upload-time = "2026-01-18T20:55:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/87/4c/27a95466354606b256f24fad464d7c97ab62bce6cc529dd4673e1179b8fb/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:292410a7d23de9b40444636b9b8f1e4e4b814af7f1ef476e44887e52a123f09d", size = 212816, upload-time = "2026-01-18T20:55:23.501Z" }, + { url = "https://files.pythonhosted.org/packages/73/cd/29cee6007bddf7a834e6cd6f536754c0535fcb939d384f0f37a38b1cddb8/ormsgpack-1.12.2-cp314-cp314t-win_amd64.whl", hash = "sha256:837dd316584485b72ef451d08dd3e96c4a11d12e4963aedb40e08f89685d8ec2", size = 117232, upload-time = "2026-01-18T20:55:45.448Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + +[[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.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +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/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-acp" +source = { editable = "packages/adapters/pydantic-acp" } +dependencies = [ + { name = "agent-client-protocol" }, + { name = "anyio" }, + { name = "pydantic" }, + { name = "pydantic-ai-slim" }, + { name = "typing-extensions" }, +] + +[package.metadata] +requires-dist = [ + { name = "agent-client-protocol", specifier = "==0.9.0" }, + { name = "anyio", specifier = ">=4.0.0" }, + { name = "pydantic", specifier = ">=2.7" }, + { name = "pydantic-ai-slim", specifier = "==1.92.0" }, + { name = "typing-extensions", specifier = ">=4.12.0" }, +] + +[[package]] +name = "pydantic-ai-slim" +version = "1.92.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "genai-prices" }, + { name = "griffelib" }, + { name = "httpx" }, + { name = "opentelemetry-api" }, + { name = "pydantic" }, + { name = "pydantic-graph" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/f9/0d54e3d8a6d41911b8c9522e0db2c14a10a3eaeab61e85bba1969459b46f/pydantic_ai_slim-1.92.0.tar.gz", hash = "sha256:3eb4cece8ad35694d42644207e60442c8211ebd0d2c5ff5bbf2aa52a091c4c04", size = 627527, upload-time = "2026-05-08T01:30:32.577Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/ba/770fc43304ee8de96bd9656189320cd385031a2edfe886be0008831b4aad/pydantic_ai_slim-1.92.0-py3-none-any.whl", hash = "sha256:a75573d92f1a8e92f5e9f4f75f9fc11199ee30619d29e21b7a8fe2b968e040ed", size = 793001, upload-time = "2026-05-08T01:30:24.904Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" }, + { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" }, + { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" }, + { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" }, + { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" }, + { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" }, + { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" }, + { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" }, + { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" }, + { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" }, + { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, + { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" }, + { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" }, + { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" }, + { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" }, + { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" }, +] + +[[package]] +name = "pydantic-graph" +version = "1.92.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "logfire-api" }, + { name = "pydantic" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/55/c439f68ba984399ed4bcf240ee074e1d68b89536e0d6ad6e606fdf00d815/pydantic_graph-1.92.0.tar.gz", hash = "sha256:ab3fc2c8ca1728875624552fe80d4aa0bd2f9ff62a8e7d809850601c4e04e796", size = 59255, upload-time = "2026-05-08T01:30:34.789Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/28/dc5723d0a03e943e0f863b9f5f738e5104ce57404006e5e7df22aa31a31e/pydantic_graph-1.92.0-py3-none-any.whl", hash = "sha256:f9278fc094103b9f9ef9b608f39ab239c7e6593103df7adc5c3a1335da9ba005", size = 73066, upload-time = "2026-05-08T01:30:28.143Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.21.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/08/f1c908c581fd11913da4711ea7ba32c0eee40b0190000996bb863b0c9349/pymdown_extensions-10.21.2.tar.gz", hash = "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc", size = 853922, upload-time = "2026-03-29T15:01:55.233Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638", size = 268901, upload-time = "2026-03-29T15:01:53.244Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +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/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.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/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[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-discovery" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/e0/cc5a8653e9a24f6cf84768f05064aa8ed5a83dcefd5e2a043db14a1c5f44/python_discovery-1.3.0.tar.gz", hash = "sha256:d098f1e86be5d45fe4d14bf1029294aabbd332f4321179dec85e76cddce834b0", size = 63925, upload-time = "2026-05-05T14:38:39.769Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/d4/24d543ab8b8158b7f5a97113c831205f5c900c92c8762b1e7f44b7ea0405/python_discovery-1.3.0-py3-none-any.whl", hash = "sha256:441d9ced3dfce36e113beb35ca302c71c7ef06f3c0f9c227a0b9bb3bd49b9e9f", size = 33124, upload-time = "2026-05-05T14:38:38.539Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[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 = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + +[[package]] +name = "regex" +version = "2026.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/3a246dbf05666918bd3664d9d787f84a9108f6f43cc953a077e4a7dfdb7e/regex-2026.4.4.tar.gz", hash = "sha256:e08270659717f6973523ce3afbafa53515c4dc5dcad637dc215b6fd50f689423", size = 416000, upload-time = "2026-04-03T20:56:28.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/7a/617356cbecdb452812a5d42f720d6d5096b360d4a4c1073af700ea140ad2/regex-2026.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b4c36a85b00fadb85db9d9e90144af0a980e1a3d2ef9cd0f8a5bef88054657c6", size = 489415, upload-time = "2026-04-03T20:53:11.645Z" }, + { url = "https://files.pythonhosted.org/packages/20/e6/bf057227144d02e3ba758b66649e87531d744dda5f3254f48660f18ae9d8/regex-2026.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dcb5453ecf9cd58b562967badd1edbf092b0588a3af9e32ee3d05c985077ce87", size = 291205, upload-time = "2026-04-03T20:53:13.289Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3b/637181b787dd1a820ba1c712cee2b4144cd84a32dc776ca067b12b2d70c8/regex-2026.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6aa809ed4dc3706cc38594d67e641601bd2f36d5555b2780ff074edfcb136cf8", size = 289225, upload-time = "2026-04-03T20:53:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/05/21/bac05d806ed02cd4b39d9c8e5b5f9a2998c94c3a351b7792e80671fa5315/regex-2026.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33424f5188a7db12958246a54f59a435b6cb62c5cf9c8d71f7cc49475a5fdada", size = 792434, upload-time = "2026-04-03T20:53:17.414Z" }, + { url = "https://files.pythonhosted.org/packages/d9/17/c65d1d8ae90b772d5758eb4014e1e011bb2db353fc4455432e6cc9100df7/regex-2026.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d346fccdde28abba117cc9edc696b9518c3307fbfcb689e549d9b5979018c6d", size = 861730, upload-time = "2026-04-03T20:53:18.903Z" }, + { url = "https://files.pythonhosted.org/packages/ad/64/933321aa082a2c6ee2785f22776143ba89840189c20d3b6b1d12b6aae16b/regex-2026.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:415a994b536440f5011aa77e50a4274d15da3245e876e5c7f19da349caaedd87", size = 906495, upload-time = "2026-04-03T20:53:20.561Z" }, + { url = "https://files.pythonhosted.org/packages/01/ea/4c8d306e9c36ac22417336b1e02e7b358152c34dc379673f2d331143725f/regex-2026.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21e5eb86179b4c67b5759d452ea7c48eb135cd93308e7a260aa489ed2eb423a4", size = 799810, upload-time = "2026-04-03T20:53:22.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/ce/7605048f00e1379eba89d610c7d644d8f695dc9b26d3b6ecfa3132b872ff/regex-2026.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:312ec9dd1ae7d96abd8c5a36a552b2139931914407d26fba723f9e53c8186f86", size = 774242, upload-time = "2026-04-03T20:53:25.015Z" }, + { url = "https://files.pythonhosted.org/packages/e9/77/283e0d5023fde22cd9e86190d6d9beb21590a452b195ffe00274de470691/regex-2026.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0d2b28aa1354c7cd7f71b7658c4326f7facac106edd7f40eda984424229fd59", size = 781257, upload-time = "2026-04-03T20:53:26.918Z" }, + { url = "https://files.pythonhosted.org/packages/8b/fb/7f3b772be101373c8626ed34c5d727dcbb8abd42a7b1219bc25fd9a3cc04/regex-2026.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:349d7310eddff40429a099c08d995c6d4a4bfaf3ff40bd3b5e5cb5a5a3c7d453", size = 854490, upload-time = "2026-04-03T20:53:29.065Z" }, + { url = "https://files.pythonhosted.org/packages/85/30/56547b80f34f4dd2986e1cdd63b1712932f63b6c4ce2f79c50a6cd79d1c2/regex-2026.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:e7ab63e9fe45a9ec3417509e18116b367e89c9ceb6219222a3396fa30b147f80", size = 763544, upload-time = "2026-04-03T20:53:30.917Z" }, + { url = "https://files.pythonhosted.org/packages/ac/2f/ce060fdfea8eff34a8997603532e44cdb7d1f35e3bc253612a8707a90538/regex-2026.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fe896e07a5a2462308297e515c0054e9ec2dd18dfdc9427b19900b37dfe6f40b", size = 844442, upload-time = "2026-04-03T20:53:32.463Z" }, + { url = "https://files.pythonhosted.org/packages/e5/44/810cb113096a1dacbe82789fbfab2823f79d19b7f1271acecb7009ba9b88/regex-2026.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:eb59c65069498dbae3c0ef07bbe224e1eaa079825a437fb47a479f0af11f774f", size = 789162, upload-time = "2026-04-03T20:53:34.039Z" }, + { url = "https://files.pythonhosted.org/packages/20/96/9647dd7f2ecf6d9ce1fb04dfdb66910d094e10d8fe53e9c15096d8aa0bd2/regex-2026.4.4-cp311-cp311-win32.whl", hash = "sha256:2a5d273181b560ef8397c8825f2b9d57013de744da9e8257b8467e5da8599351", size = 266227, upload-time = "2026-04-03T20:53:35.601Z" }, + { url = "https://files.pythonhosted.org/packages/33/80/74e13262460530c3097ff343a17de9a34d040a5dc4de9cf3a8241faab51c/regex-2026.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:9542ccc1e689e752594309444081582f7be2fdb2df75acafea8a075108566735", size = 278399, upload-time = "2026-04-03T20:53:37.021Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3c/39f19f47f19dcefa3403f09d13562ca1c0fd07ab54db2bc03148f3f6b46a/regex-2026.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:b5f9fb784824a042be3455b53d0b112655686fdb7a91f88f095f3fee1e2a2a54", size = 270473, upload-time = "2026-04-03T20:53:38.633Z" }, + { url = "https://files.pythonhosted.org/packages/e5/28/b972a4d3df61e1d7bcf1b59fdb3cddef22f88b6be43f161bb41ebc0e4081/regex-2026.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c07ab8794fa929e58d97a0e1796b8b76f70943fa39df225ac9964615cf1f9d52", size = 490434, upload-time = "2026-04-03T20:53:40.219Z" }, + { url = "https://files.pythonhosted.org/packages/84/20/30041446cf6dc3e0eab344fc62770e84c23b6b68a3b657821f9f80cb69b4/regex-2026.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c785939dc023a1ce4ec09599c032cc9933d258a998d16ca6f2b596c010940eb", size = 292061, upload-time = "2026-04-03T20:53:41.862Z" }, + { url = "https://files.pythonhosted.org/packages/62/c8/3baa06d75c98c46d4cc4262b71fd2edb9062b5665e868bca57859dadf93a/regex-2026.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b1ce5c81c9114f1ce2f9288a51a8fd3aeea33a0cc440c415bf02da323aa0a76", size = 289628, upload-time = "2026-04-03T20:53:43.701Z" }, + { url = "https://files.pythonhosted.org/packages/31/87/3accf55634caad8c0acab23f5135ef7d4a21c39f28c55c816ae012931408/regex-2026.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:760ef21c17d8e6a4fe8cf406a97cf2806a4df93416ccc82fc98d25b1c20425be", size = 796651, upload-time = "2026-04-03T20:53:45.379Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0c/aaa2c83f34efedbf06f61cb1942c25f6cf1ee3b200f832c4d05f28306c2e/regex-2026.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7088fcdcb604a4417c208e2169715800d28838fefd7455fbe40416231d1d47c1", size = 865916, upload-time = "2026-04-03T20:53:47.064Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f6/8c6924c865124643e8f37823eca845dc27ac509b2ee58123685e71cd0279/regex-2026.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:07edca1ba687998968f7db5bc355288d0c6505caa7374f013d27356d93976d13", size = 912287, upload-time = "2026-04-03T20:53:49.422Z" }, + { url = "https://files.pythonhosted.org/packages/11/0e/a9f6f81013e0deaf559b25711623864970fe6a098314e374ccb1540a4152/regex-2026.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f657a7c1c6ec51b5e0ba97c9817d06b84ea5fa8d82e43b9405de0defdc2b9", size = 801126, upload-time = "2026-04-03T20:53:51.096Z" }, + { url = "https://files.pythonhosted.org/packages/71/61/3a0cc8af2dc0c8deb48e644dd2521f173f7e6513c6e195aad9aa8dd77ac5/regex-2026.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b69102a743e7569ebee67e634a69c4cb7e59d6fa2e1aa7d3bdbf3f61435f62d", size = 776788, upload-time = "2026-04-03T20:53:52.889Z" }, + { url = "https://files.pythonhosted.org/packages/64/0b/8bb9cbf21ef7dee58e49b0fdb066a7aded146c823202e16494a36777594f/regex-2026.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dac006c8b6dda72d86ea3d1333d45147de79a3a3f26f10c1cf9287ca4ca0ac3", size = 785184, upload-time = "2026-04-03T20:53:55.627Z" }, + { url = "https://files.pythonhosted.org/packages/99/c2/d3e80e8137b25ee06c92627de4e4d98b94830e02b3e6f81f3d2e3f504cf5/regex-2026.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:50a766ee2010d504554bfb5f578ed2e066898aa26411d57e6296230627cdefa0", size = 859913, upload-time = "2026-04-03T20:53:57.249Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/9d5d876157d969c804622456ef250017ac7a8f83e0e14f903b9e6df5ce95/regex-2026.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9e2f5217648f68e3028c823df58663587c1507a5ba8419f4fdfc8a461be76043", size = 765732, upload-time = "2026-04-03T20:53:59.428Z" }, + { url = "https://files.pythonhosted.org/packages/82/80/b568935b4421388561c8ed42aff77247285d3ae3bb2a6ca22af63bae805e/regex-2026.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39d8de85a08e32632974151ba59c6e9140646dcc36c80423962b1c5c0a92e244", size = 852152, upload-time = "2026-04-03T20:54:01.505Z" }, + { url = "https://files.pythonhosted.org/packages/39/29/f0f81217e21cd998245da047405366385d5c6072048038a3d33b37a79dc0/regex-2026.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:55d9304e0e7178dfb1e106c33edf834097ddf4a890e2f676f6c5118f84390f73", size = 789076, upload-time = "2026-04-03T20:54:03.323Z" }, + { url = "https://files.pythonhosted.org/packages/49/1d/1d957a61976ab9d4e767dd4f9d04b66cc0c41c5e36cf40e2d43688b5ae6f/regex-2026.4.4-cp312-cp312-win32.whl", hash = "sha256:04bb679bc0bde8a7bfb71e991493d47314e7b98380b083df2447cda4b6edb60f", size = 266700, upload-time = "2026-04-03T20:54:05.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/bf575d396aeb58ea13b06ef2adf624f65b70fafef6950a80fc3da9cae3bc/regex-2026.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:db0ac18435a40a2543dbb3d21e161a6c78e33e8159bd2e009343d224bb03bb1b", size = 277768, upload-time = "2026-04-03T20:54:07.312Z" }, + { url = "https://files.pythonhosted.org/packages/c9/27/049df16ec6a6828ccd72add3c7f54b4df029669bea8e9817df6fff58be90/regex-2026.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:4ce255cc05c1947a12989c6db801c96461947adb7a59990f1360b5983fab4983", size = 270568, upload-time = "2026-04-03T20:54:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/9d/83/c4373bc5f31f2cf4b66f9b7c31005bd87fe66f0dce17701f7db4ee79ee29/regex-2026.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:62f5519042c101762509b1d717b45a69c0139d60414b3c604b81328c01bd1943", size = 490273, upload-time = "2026-04-03T20:54:11.202Z" }, + { url = "https://files.pythonhosted.org/packages/46/f8/fe62afbcc3cf4ad4ac9adeaafd98aa747869ae12d3e8e2ac293d0593c435/regex-2026.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3790ba9fb5dd76715a7afe34dbe603ba03f8820764b1dc929dd08106214ed031", size = 291954, upload-time = "2026-04-03T20:54:13.412Z" }, + { url = "https://files.pythonhosted.org/packages/5a/92/4712b9fe6a33d232eeb1c189484b80c6c4b8422b90e766e1195d6e758207/regex-2026.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fae3c6e795d7678963f2170152b0d892cf6aee9ee8afc8c45e6be38d5107fe7", size = 289487, upload-time = "2026-04-03T20:54:15.824Z" }, + { url = "https://files.pythonhosted.org/packages/88/2c/f83b93f85e01168f1070f045a42d4c937b69fdb8dd7ae82d307253f7e36e/regex-2026.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:298c3ec2d53225b3bf91142eb9691025bab610e0c0c51592dde149db679b3d17", size = 796646, upload-time = "2026-04-03T20:54:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/df/55/61a2e17bf0c4dc57e11caf8dd11771280d8aaa361785f9e3bc40d653f4a7/regex-2026.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e9638791082eaf5b3ac112c587518ee78e083a11c4b28012d8fe2a0f536dfb17", size = 865904, upload-time = "2026-04-03T20:54:20.019Z" }, + { url = "https://files.pythonhosted.org/packages/45/32/1ac8ed1b5a346b5993a3d256abe0a0f03b0b73c8cc88d928537368ac65b6/regex-2026.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae3e764bd4c5ff55035dc82a8d49acceb42a5298edf6eb2fc4d328ee5dd7afae", size = 912304, upload-time = "2026-04-03T20:54:22.403Z" }, + { url = "https://files.pythonhosted.org/packages/26/47/2ee5c613ab546f0eddebf9905d23e07beb933416b1246c2d8791d01979b4/regex-2026.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffa81f81b80047ba89a3c69ae6a0f78d06f4a42ce5126b0eb2a0a10ad44e0b2e", size = 801126, upload-time = "2026-04-03T20:54:24.308Z" }, + { url = "https://files.pythonhosted.org/packages/75/cd/41dacd129ca9fd20bd7d02f83e0fad83e034ac8a084ec369c90f55ef37e2/regex-2026.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f56ebf9d70305307a707911b88469213630aba821e77de7d603f9d2f0730687d", size = 776772, upload-time = "2026-04-03T20:54:26.319Z" }, + { url = "https://files.pythonhosted.org/packages/89/6d/5af0b588174cb5f46041fa7dd64d3fd5cd2fe51f18766703d1edc387f324/regex-2026.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:773d1dfd652bbffb09336abf890bfd64785c7463716bf766d0eb3bc19c8b7f27", size = 785228, upload-time = "2026-04-03T20:54:28.387Z" }, + { url = "https://files.pythonhosted.org/packages/b7/3b/f5a72b7045bd59575fc33bf1345f156fcfd5a8484aea6ad84b12c5a82114/regex-2026.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d51d20befd5275d092cdffba57ded05f3c436317ee56466c8928ac32d960edaf", size = 860032, upload-time = "2026-04-03T20:54:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/39/a4/72a317003d6fcd7a573584a85f59f525dfe8f67e355ca74eb6b53d66a5e2/regex-2026.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0a51cdb3c1e9161154f976cb2bef9894bc063ac82f31b733087ffb8e880137d0", size = 765714, upload-time = "2026-04-03T20:54:32.789Z" }, + { url = "https://files.pythonhosted.org/packages/25/1e/5672e16f34dbbcb2560cc7e6a2fbb26dfa8b270711e730101da4423d3973/regex-2026.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ae5266a82596114e41fb5302140e9630204c1b5f325c770bec654b95dd54b0aa", size = 852078, upload-time = "2026-04-03T20:54:34.546Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0d/c813f0af7c6cc7ed7b9558bac2e5120b60ad0fa48f813e4d4bd55446f214/regex-2026.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c882cd92ec68585e9c1cf36c447ec846c0d94edd706fe59e0c198e65822fd23b", size = 789181, upload-time = "2026-04-03T20:54:36.642Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6d/a344608d1adbd2a95090ddd906cec09a11be0e6517e878d02a5123e0917f/regex-2026.4.4-cp313-cp313-win32.whl", hash = "sha256:05568c4fbf3cb4fa9e28e3af198c40d3237cf6041608a9022285fe567ec3ad62", size = 266690, upload-time = "2026-04-03T20:54:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/31/07/54049f89b46235ca6f45cd6c88668a7050e77d4a15555e47dd40fde75263/regex-2026.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:3384df51ed52db0bea967e21458ab0a414f67cdddfd94401688274e55147bb81", size = 277733, upload-time = "2026-04-03T20:54:40.11Z" }, + { url = "https://files.pythonhosted.org/packages/0e/21/61366a8e20f4d43fb597708cac7f0e2baadb491ecc9549b4980b2be27d16/regex-2026.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:acd38177bd2c8e69a411d6521760806042e244d0ef94e2dd03ecdaa8a3c99427", size = 270565, upload-time = "2026-04-03T20:54:41.883Z" }, + { url = "https://files.pythonhosted.org/packages/f1/1e/3a2b9672433bef02f5d39aa1143ca2c08f311c1d041c464a42be9ae648dc/regex-2026.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f94a11a9d05afcfcfa640e096319720a19cc0c9f7768e1a61fceee6a3afc6c7c", size = 494126, upload-time = "2026-04-03T20:54:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/4e/4b/c132a4f4fe18ad3340d89fcb56235132b69559136036b845be3c073142ed/regex-2026.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:36bcb9d6d1307ab629edc553775baada2aefa5c50ccc0215fbfd2afcfff43141", size = 293882, upload-time = "2026-04-03T20:54:45.41Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5f/eaa38092ce7a023656280f2341dbbd4ad5f05d780a70abba7bb4f4bea54c/regex-2026.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261c015b3e2ed0919157046d768774ecde57f03d8fa4ba78d29793447f70e717", size = 292334, upload-time = "2026-04-03T20:54:47.051Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f6/dd38146af1392dac33db7074ab331cec23cced3759167735c42c5460a243/regex-2026.4.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c228cf65b4a54583763645dcd73819b3b381ca8b4bb1b349dee1c135f4112c07", size = 811691, upload-time = "2026-04-03T20:54:49.074Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f0/dc54c2e69f5eeec50601054998ec3690d5344277e782bd717e49867c1d29/regex-2026.4.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dd2630faeb6876fb0c287f664d93ddce4d50cd46c6e88e60378c05c9047e08ca", size = 871227, upload-time = "2026-04-03T20:54:51.035Z" }, + { url = "https://files.pythonhosted.org/packages/a1/af/cb16bd5dc61621e27df919a4449bbb7e5a1034c34d307e0a706e9cc0f3e3/regex-2026.4.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a50ab11b7779b849472337191f3a043e27e17f71555f98d0092fa6d73364520", size = 917435, upload-time = "2026-04-03T20:54:52.994Z" }, + { url = "https://files.pythonhosted.org/packages/5c/71/8b260897f22996b666edd9402861668f45a2ca259f665ac029e6104a2d7d/regex-2026.4.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0734f63afe785138549fbe822a8cfeaccd1bae814c5057cc0ed5b9f2de4fc883", size = 816358, upload-time = "2026-04-03T20:54:54.884Z" }, + { url = "https://files.pythonhosted.org/packages/1c/60/775f7f72a510ef238254906c2f3d737fc80b16ca85f07d20e318d2eea894/regex-2026.4.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4ee50606cb1967db7e523224e05f32089101945f859928e65657a2cbb3d278b", size = 785549, upload-time = "2026-04-03T20:54:57.01Z" }, + { url = "https://files.pythonhosted.org/packages/58/42/34d289b3627c03cf381e44da534a0021664188fa49ba41513da0b4ec6776/regex-2026.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6c1818f37be3ca02dcb76d63f2c7aaba4b0dc171b579796c6fbe00148dfec6b1", size = 801364, upload-time = "2026-04-03T20:54:58.981Z" }, + { url = "https://files.pythonhosted.org/packages/fc/20/f6ecf319b382a8f1ab529e898b222c3f30600fcede7834733c26279e7465/regex-2026.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f5bfc2741d150d0be3e4a0401a5c22b06e60acb9aa4daa46d9e79a6dcd0f135b", size = 866221, upload-time = "2026-04-03T20:55:00.88Z" }, + { url = "https://files.pythonhosted.org/packages/92/6a/9f16d3609d549bd96d7a0b2aee1625d7512ba6a03efc01652149ef88e74d/regex-2026.4.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:504ffa8a03609a087cad81277a629b6ce884b51a24bd388a7980ad61748618ff", size = 772530, upload-time = "2026-04-03T20:55:03.213Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f6/aa9768bc96a4c361ac96419fbaf2dcdc33970bb813df3ba9b09d5d7b6d96/regex-2026.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70aadc6ff12e4b444586e57fc30771f86253f9f0045b29016b9605b4be5f7dfb", size = 856989, upload-time = "2026-04-03T20:55:05.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b4/c671db3556be2473ae3e4bb7a297c518d281452871501221251ea4ecba57/regex-2026.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f4f83781191007b6ef43b03debc35435f10cad9b96e16d147efe84a1d48bdde4", size = 803241, upload-time = "2026-04-03T20:55:07.162Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5c/83e3b1d89fa4f6e5a1bc97b4abd4a9a97b3c1ac7854164f694f5f0ba98a0/regex-2026.4.4-cp313-cp313t-win32.whl", hash = "sha256:e014a797de43d1847df957c0a2a8e861d1c17547ee08467d1db2c370b7568baa", size = 269921, upload-time = "2026-04-03T20:55:09.62Z" }, + { url = "https://files.pythonhosted.org/packages/28/07/077c387121f42cdb4d92b1301133c0d93b5709d096d1669ab847dda9fe2e/regex-2026.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:b15b88b0d52b179712632832c1d6e58e5774f93717849a41096880442da41ab0", size = 281240, upload-time = "2026-04-03T20:55:11.521Z" }, + { url = "https://files.pythonhosted.org/packages/9d/22/ead4a4abc7c59a4d882662aa292ca02c8b617f30b6e163bc1728879e9353/regex-2026.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:586b89cdadf7d67bf86ae3342a4dcd2b8d70a832d90c18a0ae955105caf34dbe", size = 272440, upload-time = "2026-04-03T20:55:13.365Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f5/ed97c2dc47b5fbd4b73c0d7d75f9ebc8eca139f2bbef476bba35f28c0a77/regex-2026.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2da82d643fa698e5e5210e54af90181603d5853cf469f5eedf9bfc8f59b4b8c7", size = 490343, upload-time = "2026-04-03T20:55:15.241Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/de4828a7385ec166d673a5790ad06ac48cdaa98bc0960108dd4b9cc1aef7/regex-2026.4.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:54a1189ad9d9357760557c91103d5e421f0a2dabe68a5cdf9103d0dcf4e00752", size = 291909, upload-time = "2026-04-03T20:55:17.558Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d6/5cfbfc97f3201a4d24b596a77957e092030dcc4205894bc035cedcfce62f/regex-2026.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:76d67d5afb1fe402d10a6403bae668d000441e2ab115191a804287d53b772951", size = 289692, upload-time = "2026-04-03T20:55:20.561Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/f2212d9fd56fe897e36d0110ba30ba2d247bd6410c5bd98499c7e5a1e1f2/regex-2026.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7cd3e4ee8d80447a83bbc9ab0c8459781fa77087f856c3e740d7763be0df27f", size = 796979, upload-time = "2026-04-03T20:55:22.56Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e3/a016c12675fbac988a60c7e1c16e67823ff0bc016beb27bd7a001dbdabc6/regex-2026.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e19e18c568d2866d8b6a6dfad823db86193503f90823a8f66689315ba28fbe8", size = 866744, upload-time = "2026-04-03T20:55:24.646Z" }, + { url = "https://files.pythonhosted.org/packages/af/a4/0b90ca4cf17adc3cb43de80ec71018c37c88ad64987e8d0d481a95ca60b5/regex-2026.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7698a6f38730fd1385d390d1ed07bb13dce39aa616aca6a6d89bea178464b9a4", size = 911613, upload-time = "2026-04-03T20:55:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3b/2b3dac0b82d41ab43aa87c6ecde63d71189d03fe8854b8ca455a315edac3/regex-2026.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:173a66f3651cdb761018078e2d9487f4cf971232c990035ec0eb1cdc6bf929a9", size = 800551, upload-time = "2026-04-03T20:55:29.532Z" }, + { url = "https://files.pythonhosted.org/packages/25/fe/5365eb7aa0e753c4b5957815c321519ecab033c279c60e1b1ae2367fa810/regex-2026.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa7922bbb2cc84fa062d37723f199d4c0cd200245ce269c05db82d904db66b83", size = 776911, upload-time = "2026-04-03T20:55:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b3/7fb0072156bba065e3b778a7bc7b0a6328212be5dd6a86fd207e0c4f2dab/regex-2026.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:59f67cd0a0acaf0e564c20bbd7f767286f23e91e2572c5703bf3e56ea7557edb", size = 785751, upload-time = "2026-04-03T20:55:33.797Z" }, + { url = "https://files.pythonhosted.org/packages/02/1a/9f83677eb699273e56e858f7bd95acdbee376d42f59e8bfca2fd80d79df3/regex-2026.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:475e50f3f73f73614f7cba5524d6de49dee269df00272a1b85e3d19f6d498465", size = 860484, upload-time = "2026-04-03T20:55:35.745Z" }, + { url = "https://files.pythonhosted.org/packages/3b/7a/93937507b61cfcff8b4c5857f1b452852b09f741daa9acae15c971d8554e/regex-2026.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a1c0c7d67b64d85ac2e1879923bad2f08a08f3004055f2f406ef73c850114bd4", size = 765939, upload-time = "2026-04-03T20:55:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/86/ea/81a7f968a351c6552b1670ead861e2a385be730ee28402233020c67f9e0f/regex-2026.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:1371c2ccbb744d66ee63631cc9ca12aa233d5749972626b68fe1a649dd98e566", size = 851417, upload-time = "2026-04-03T20:55:39.92Z" }, + { url = "https://files.pythonhosted.org/packages/4c/7e/323c18ce4b5b8f44517a36342961a0306e931e499febbd876bb149d900f0/regex-2026.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59968142787042db793348a3f5b918cf24ced1f23247328530e063f89c128a95", size = 789056, upload-time = "2026-04-03T20:55:42.303Z" }, + { url = "https://files.pythonhosted.org/packages/c0/af/e7510f9b11b1913b0cd44eddb784b2d650b2af6515bfce4cffcc5bfd1d38/regex-2026.4.4-cp314-cp314-win32.whl", hash = "sha256:59efe72d37fd5a91e373e5146f187f921f365f4abc1249a5ab446a60f30dd5f8", size = 272130, upload-time = "2026-04-03T20:55:44.995Z" }, + { url = "https://files.pythonhosted.org/packages/9a/51/57dae534c915e2d3a21490e88836fa2ae79dde3b66255ecc0c0a155d2c10/regex-2026.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:e0aab3ff447845049d676827d2ff714aab4f73f340e155b7de7458cf53baa5a4", size = 280992, upload-time = "2026-04-03T20:55:47.316Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5e/abaf9f4c3792e34edb1434f06717fae2b07888d85cb5cec29f9204931bf8/regex-2026.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:a7a5bb6aa0cf62208bb4fa079b0c756734f8ad0e333b425732e8609bd51ee22f", size = 273563, upload-time = "2026-04-03T20:55:49.273Z" }, + { url = "https://files.pythonhosted.org/packages/ff/06/35da85f9f217b9538b99cbb170738993bcc3b23784322decb77619f11502/regex-2026.4.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:97850d0638391bdc7d35dc1c1039974dcb921eaafa8cc935ae4d7f272b1d60b3", size = 494191, upload-time = "2026-04-03T20:55:51.258Z" }, + { url = "https://files.pythonhosted.org/packages/54/5b/1bc35f479eef8285c4baf88d8c002023efdeebb7b44a8735b36195486ae7/regex-2026.4.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ee7337f88f2a580679f7bbfe69dc86c043954f9f9c541012f49abc554a962f2e", size = 293877, upload-time = "2026-04-03T20:55:53.214Z" }, + { url = "https://files.pythonhosted.org/packages/39/5b/f53b9ad17480b3ddd14c90da04bfb55ac6894b129e5dea87bcaf7d00e336/regex-2026.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7429f4e6192c11d659900c0648ba8776243bf396ab95558b8c51a345afeddde6", size = 292410, upload-time = "2026-04-03T20:55:55.736Z" }, + { url = "https://files.pythonhosted.org/packages/bb/56/52377f59f60a7c51aa4161eecf0b6032c20b461805aca051250da435ffc9/regex-2026.4.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4f10fbd5dd13dcf4265b4cc07d69ca70280742870c97ae10093e3d66000359", size = 811831, upload-time = "2026-04-03T20:55:57.802Z" }, + { url = "https://files.pythonhosted.org/packages/dd/63/8026310bf066f702a9c361f83a8c9658f3fe4edb349f9c1e5d5273b7c40c/regex-2026.4.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a152560af4f9742b96f3827090f866eeec5becd4765c8e0d3473d9d280e76a5a", size = 871199, upload-time = "2026-04-03T20:56:00.333Z" }, + { url = "https://files.pythonhosted.org/packages/20/9f/a514bbb00a466dbb506d43f187a04047f7be1505f10a9a15615ead5080ee/regex-2026.4.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54170b3e95339f415d54651f97df3bff7434a663912f9358237941bbf9143f55", size = 917649, upload-time = "2026-04-03T20:56:02.445Z" }, + { url = "https://files.pythonhosted.org/packages/cb/6b/8399f68dd41a2030218839b9b18360d79b86d22b9fab5ef477c7f23ca67c/regex-2026.4.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:07f190d65f5a72dcb9cf7106bfc3d21e7a49dd2879eda2207b683f32165e4d99", size = 816388, upload-time = "2026-04-03T20:56:04.595Z" }, + { url = "https://files.pythonhosted.org/packages/1e/9c/103963f47c24339a483b05edd568594c2be486188f688c0170fd504b2948/regex-2026.4.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9a2741ce5a29d3c84b0b94261ba630ab459a1b847a0d6beca7d62d188175c790", size = 785746, upload-time = "2026-04-03T20:56:07.13Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ee/7f6054c0dec0cee3463c304405e4ff42e27cff05bf36fcb34be549ab17bd/regex-2026.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b26c30df3a28fd9793113dac7385a4deb7294a06c0f760dd2b008bd49a9139bc", size = 801483, upload-time = "2026-04-03T20:56:09.365Z" }, + { url = "https://files.pythonhosted.org/packages/30/c2/51d3d941cf6070dc00c3338ecf138615fc3cce0421c3df6abe97a08af61a/regex-2026.4.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:421439d1bee44b19f4583ccf42670ca464ffb90e9fdc38d37f39d1ddd1e44f1f", size = 866331, upload-time = "2026-04-03T20:56:12.039Z" }, + { url = "https://files.pythonhosted.org/packages/16/e8/76d50dcc122ac33927d939f350eebcfe3dbcbda96913e03433fc36de5e63/regex-2026.4.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b40379b53ecbc747fd9bdf4a0ea14eb8188ca1bd0f54f78893a39024b28f4863", size = 772673, upload-time = "2026-04-03T20:56:14.558Z" }, + { url = "https://files.pythonhosted.org/packages/a5/6e/5f6bf75e20ea6873d05ba4ec78378c375cbe08cdec571c83fbb01606e563/regex-2026.4.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:08c55c13d2eef54f73eeadc33146fb0baaa49e7335eb1aff6ae1324bf0ddbe4a", size = 857146, upload-time = "2026-04-03T20:56:16.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/33/3c76d9962949e487ebba353a18e89399f292287204ac8f2f4cfc3a51c233/regex-2026.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9776b85f510062f5a75ef112afe5f494ef1635607bf1cc220c1391e9ac2f5e81", size = 803463, upload-time = "2026-04-03T20:56:18.923Z" }, + { url = "https://files.pythonhosted.org/packages/19/eb/ef32dcd2cb69b69bc0c3e55205bce94a7def48d495358946bc42186dcccc/regex-2026.4.4-cp314-cp314t-win32.whl", hash = "sha256:385edaebde5db5be103577afc8699fea73a0e36a734ba24870be7ffa61119d74", size = 275709, upload-time = "2026-04-03T20:56:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/a0/86/c291bf740945acbf35ed7dbebf8e2eea2f3f78041f6bd7cdab80cb274dc0/regex-2026.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:5d354b18839328927832e2fa5f7c95b7a3ccc39e7a681529e1685898e6436d45", size = 285622, upload-time = "2026-04-03T20:56:23.641Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e7/ec846d560ae6a597115153c02ca6138a7877a1748b2072d9521c10a93e58/regex-2026.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:af0384cb01a33600c49505c27c6c57ab0b27bf84a74e28524c92ca897ebdac9d", size = 275773, upload-time = "2026-04-03T20:56:26.07Z" }, +] + +[[package]] +name = "requests" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" }, + { url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" }, + { url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" }, + { url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" }, + { url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" }, + { url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" }, + { url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" }, + { url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" }, + { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" }, +] + +[[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 = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, +] + +[[package]] +name = "tiktoken" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, + { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, + { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, + { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422, upload-time = "2025-10-06T20:21:51.734Z" }, + { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, + { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, + { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, + { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, + { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, + { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, + { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, + { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, + { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, + { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, + { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, + { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, + { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, + { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, + { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, + { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, + { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, + { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, + { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, + { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, + { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "ty" +version = "0.0.34" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/69/e24eefe2c35c0fdbdec9b60e162727af669bb76d64d993d982eb67b24c38/ty-0.0.34.tar.gz", hash = "sha256:a6efe66b0f13c03a65e6c72ec9abfe2792e2fd063c74fa67e2c4930e29d661be", size = 5585933, upload-time = "2026-05-01T23:06:46.388Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/7b/8b85003d6639ef17a97dcbb31f4511cfe78f1c81a964470db100c8c883e7/ty-0.0.34-py3-none-linux_armv6l.whl", hash = "sha256:9ecc3d14f07a95a6ceb88e07f8e62358dbd37325d3d5bd56da7217ff1fef7fb8", size = 11067094, upload-time = "2026-05-01T23:06:21.133Z" }, + { url = "https://files.pythonhosted.org/packages/d7/25/b0098f65b020b015c40567c763fc66fffbec88b2ba6f584bca1e92f05ebb/ty-0.0.34-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0dccffd8a9d02321cd2dee3249df205e26d62694e741f4eeca36b157fd8b419f", size = 10840909, upload-time = "2026-05-01T23:06:18.409Z" }, + { url = "https://files.pythonhosted.org/packages/e4/55/5e4adcf7d2a1006b844903b27cb81244a9b748d850433a46a6c21776c401/ty-0.0.34-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b0ea47a2998e167ab3b21d2f4b5309a9cf33c297809f6d7e3e753252223174d0", size = 10279378, upload-time = "2026-05-01T23:06:37.962Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/f537dca0db8fe2558e8ab04d8941d687b384fcc1df5eb9023b2db75ac26c/ty-0.0.34-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b37da00b41a118a459ae56d8947e70651073fb33ebfbceb820e4a10b22d5023", size = 10817423, upload-time = "2026-05-01T23:06:26.247Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c4/55a3ad1da2815af1009bdc1b8c90dc11a364cd314e4b48c5128ba9d38859/ty-0.0.34-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:81cbbb93c2342fe3de43e625d3a9eb149633e9f485e816ebf6395d08685355d8", size = 10851826, upload-time = "2026-05-01T23:06:24.198Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8c/9c7606af22d73fb43ea4369472d9c66ece11231be73b0efe8e3c61655559/ty-0.0.34-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c5b4dea1594a021289e172582df9cde7089dce14b276fc650e7b212b1772e12", size = 11356318, upload-time = "2026-05-01T23:06:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/20/54/bb423f663721ab4138b216425c6b55eaefd3a068243b24d6d8fe988f4e13/ty-0.0.34-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:030fb00aa2d2a5b5ae9d9183d574e0c82dae80566700a7490c43669d8ece40cd", size = 11902968, upload-time = "2026-05-01T23:06:35.82Z" }, + { url = "https://files.pythonhosted.org/packages/b6/22/01122b21ab6b534a2f618c6bbe5f1f7f49fd56f4b2ec8887cd6d40d08fb3/ty-0.0.34-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ae9555e24e36c63a8218e037a5a63f15579eb6aa94f41017e57cd41d335cfb5", size = 11548860, upload-time = "2026-05-01T23:06:42.155Z" }, + { url = "https://files.pythonhosted.org/packages/d1/50/86008b1392ec64bed1957bbcc7aaa43b466b50dfc91bb131841c21d7c5c3/ty-0.0.34-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99eb23df9ed129fc26d1ab00d6f0b8dfe5253b09c2ac6abdb11523fa70d67f10", size = 11457097, upload-time = "2026-05-01T23:06:53.477Z" }, + { url = "https://files.pythonhosted.org/packages/92/3e/4558b2296963ba99c58d8409c57d7db4f3061b656c3613cb21c02c1ef4c2/ty-0.0.34-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:85de45382016eceae69e104815eb2cfa200787df104002e262a86cbd43ed2c02", size = 10798192, upload-time = "2026-05-01T23:06:40.004Z" }, + { url = "https://files.pythonhosted.org/packages/76/bf/650d24402be2ef678528d60caac1d9477a40fc37e3792ecef07834fd7a4a/ty-0.0.34-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:14cb575fb8fa5131f5129d100cfe23c1575d23faf5dfc5158432749a3e38c9b5", size = 10890390, upload-time = "2026-05-01T23:06:33.076Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ef/ccd2ca13906079f7935fd7e067661b24233017f57d987d51d6a121d85bb5/ty-0.0.34-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c6fc0b69d8450e6910ba9db34572b959b81329a97ae273c391f70e9fb6c1aade", size = 11031564, upload-time = "2026-05-01T23:06:55.812Z" }, + { url = "https://files.pythonhosted.org/packages/ba/2d/d27b72005b6f43599e3bcabab0d7135ac0c230b7a307bb99f9eea02c1cda/ty-0.0.34-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:30dfcec2f0fde3993f4f912ed0e057dcbebc8615299f610a4c2ddb7b5a3e1e06", size = 11553430, upload-time = "2026-05-01T23:06:31.096Z" }, + { url = "https://files.pythonhosted.org/packages/a7/12/20812e1ad930b8d4af70eebf19ad23cff6e31efcfa613ef884531fcdbaa1/ty-0.0.34-py3-none-win32.whl", hash = "sha256:97b77ddf007271b812a313a8f0a14929bc5590958433e1fb83ef585676f53342", size = 10436048, upload-time = "2026-05-01T23:06:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/afa095c5987868fbda27c0f731146ac8e3d07b357adfa83daccaee5b1a16/ty-0.0.34-py3-none-win_amd64.whl", hash = "sha256:1f543968accb952705134028d1fda8656882787dbbc667ad4d6c3ba23791d604", size = 11462526, upload-time = "2026-05-01T23:06:28.514Z" }, + { url = "https://files.pythonhosted.org/packages/63/8f/bf041a06260d77662c0605e56dacfe90b786bf824cbe1aed238d15fe5e84/ty-0.0.34-py3-none-win_arm64.whl", hash = "sha256:ea09108cbcb16b6b06d7596312b433bf49681e78d30e4dc7fb3c1b248a95e09a", size = 10846945, upload-time = "2026-05-01T23:06:44.428Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "uuid-utils" +version = "0.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/d1/38a573f0c631c062cf42fa1f5d021d4dd3c31fb23e4376e4b56b0c9fbbed/uuid_utils-0.14.1.tar.gz", hash = "sha256:9bfc95f64af80ccf129c604fb6b8ca66c6f256451e32bc4570f760e4309c9b69", size = 22195, upload-time = "2026-02-20T22:50:38.833Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/b7/add4363039a34506a58457d96d4aa2126061df3a143eb4d042aedd6a2e76/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:93a3b5dc798a54a1feb693f2d1cb4cf08258c32ff05ae4929b5f0a2ca624a4f0", size = 604679, upload-time = "2026-02-20T22:50:27.469Z" }, + { url = "https://files.pythonhosted.org/packages/dd/84/d1d0bef50d9e66d31b2019997c741b42274d53dde2e001b7a83e9511c339/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ccd65a4b8e83af23eae5e56d88034b2fe7264f465d3e830845f10d1591b81741", size = 309346, upload-time = "2026-02-20T22:50:31.857Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ed/b6d6fd52a6636d7c3eddf97d68da50910bf17cd5ac221992506fb56cf12e/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b56b0cacd81583834820588378e432b0696186683b813058b707aedc1e16c4b1", size = 344714, upload-time = "2026-02-20T22:50:42.642Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a7/a19a1719fb626fe0b31882db36056d44fe904dc0cf15b06fdf56b2679cf7/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb3cf14de789097320a3c56bfdfdd51b1225d11d67298afbedee7e84e3837c96", size = 350914, upload-time = "2026-02-20T22:50:36.487Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fc/f6690e667fdc3bb1a73f57951f97497771c56fe23e3d302d7404be394d4f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e0854a90d67f4b0cc6e54773deb8be618f4c9bad98d3326f081423b5d14fae", size = 482609, upload-time = "2026-02-20T22:50:37.511Z" }, + { url = "https://files.pythonhosted.org/packages/54/6e/dcd3fa031320921a12ec7b4672dea3bd1dd90ddffa363a91831ba834d559/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6743ba194de3910b5feb1a62590cd2587e33a73ab6af8a01b642ceb5055862", size = 345699, upload-time = "2026-02-20T22:50:46.87Z" }, + { url = "https://files.pythonhosted.org/packages/04/28/e5220204b58b44ac0047226a9d016a113fde039280cc8732d9e6da43b39f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:043fb58fde6cf1620a6c066382f04f87a8e74feb0f95a585e4ed46f5d44af57b", size = 372205, upload-time = "2026-02-20T22:50:28.438Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d9/3d2eb98af94b8dfffc82b6a33b4dfc87b0a5de2c68a28f6dde0db1f8681b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c915d53f22945e55fe0d3d3b0b87fd965a57f5fd15666fd92d6593a73b1dd297", size = 521836, upload-time = "2026-02-20T22:50:23.057Z" }, + { url = "https://files.pythonhosted.org/packages/a8/15/0eb106cc6fe182f7577bc0ab6e2f0a40be247f35c5e297dbf7bbc460bd02/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0972488e3f9b449e83f006ead5a0e0a33ad4a13e4462e865b7c286ab7d7566a3", size = 625260, upload-time = "2026-02-20T22:50:25.949Z" }, + { url = "https://files.pythonhosted.org/packages/3c/17/f539507091334b109e7496830af2f093d9fc8082411eafd3ece58af1f8ba/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:1c238812ae0c8ffe77d8d447a32c6dfd058ea4631246b08b5a71df586ff08531", size = 587824, upload-time = "2026-02-20T22:50:35.225Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c2/d37a7b2e41f153519367d4db01f0526e0d4b06f1a4a87f1c5dfca5d70a8b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bec8f8ef627af86abf8298e7ec50926627e29b34fa907fcfbedb45aaa72bca43", size = 551407, upload-time = "2026-02-20T22:50:44.915Z" }, + { url = "https://files.pythonhosted.org/packages/65/36/2d24b2cbe78547c6532da33fb8613debd3126eccc33a6374ab788f5e46e9/uuid_utils-0.14.1-cp39-abi3-win32.whl", hash = "sha256:b54d6aa6252d96bac1fdbc80d26ba71bad9f220b2724d692ad2f2310c22ef523", size = 183476, upload-time = "2026-02-20T22:50:32.745Z" }, + { url = "https://files.pythonhosted.org/packages/83/92/2d7e90df8b1a69ec4cff33243ce02b7a62f926ef9e2f0eca5a026889cd73/uuid_utils-0.14.1-cp39-abi3-win_amd64.whl", hash = "sha256:fc27638c2ce267a0ce3e06828aff786f91367f093c80625ee21dad0208e0f5ba", size = 187147, upload-time = "2026-02-20T22:50:45.807Z" }, + { url = "https://files.pythonhosted.org/packages/d9/26/529f4beee17e5248e37e0bc17a2761d34c0fa3b1e5729c88adb2065bae6e/uuid_utils-0.14.1-cp39-abi3-win_arm64.whl", hash = "sha256:b04cb49b42afbc4ff8dbc60cf054930afc479d6f4dd7f1ec3bbe5dbfdde06b7a", size = 188132, upload-time = "2026-02-20T22:50:41.718Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/6c64bdbf71f58ccde7919e00491812556f446a5291573af92c49a5e9aaef/uuid_utils-0.14.1-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b197cd5424cf89fb019ca7f53641d05bfe34b1879614bed111c9c313b5574cd8", size = 591617, upload-time = "2026-02-20T22:50:24.532Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f0/758c3b0fb0c4871c7704fef26a5bc861de4f8a68e4831669883bebe07b0f/uuid_utils-0.14.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:12c65020ba6cb6abe1d57fcbfc2d0ea0506c67049ee031714057f5caf0f9bc9c", size = 303702, upload-time = "2026-02-20T22:50:40.687Z" }, + { url = "https://files.pythonhosted.org/packages/85/89/d91862b544c695cd58855efe3201f83894ed82fffe34500774238ab8eba7/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b5d2ad28063d422ccc2c28d46471d47b61a58de885d35113a8f18cb547e25bf", size = 337678, upload-time = "2026-02-20T22:50:39.768Z" }, + { url = "https://files.pythonhosted.org/packages/ee/6b/cf342ba8a898f1de024be0243fac67c025cad530c79ea7f89c4ce718891a/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da2234387b45fde40b0fedfee64a0ba591caeea9c48c7698ab6e2d85c7991533", size = 343711, upload-time = "2026-02-20T22:50:43.965Z" }, + { url = "https://files.pythonhosted.org/packages/b3/20/049418d094d396dfa6606b30af925cc68a6670c3b9103b23e6990f84b589/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50fffc2827348c1e48972eed3d1c698959e63f9d030aa5dd82ba451113158a62", size = 476731, upload-time = "2026-02-20T22:50:30.589Z" }, + { url = "https://files.pythonhosted.org/packages/77/a1/0857f64d53a90321e6a46a3d4cc394f50e1366132dcd2ae147f9326ca98b/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dbe718765f70f5b7f9b7f66b6a937802941b1cc56bcf642ce0274169741e01", size = 338902, upload-time = "2026-02-20T22:50:33.927Z" }, + { url = "https://files.pythonhosted.org/packages/ed/d0/5bf7cbf1ac138c92b9ac21066d18faf4d7e7f651047b700eb192ca4b9fdb/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:258186964039a8e36db10810c1ece879d229b01331e09e9030bc5dcabe231bd2", size = 364700, upload-time = "2026-02-20T22:50:21.732Z" }, +] + +[[package]] +name = "uv" +version = "0.11.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/02/69a3b06fd8a91f95b79e95e14f5ccdd4df0f124c381aefe9d1e2784d5a65/uv-0.11.11.tar.gz", hash = "sha256:2ba46a912a1775957c579a1a42c8c8b480418502326b72427b1cad972c8f659f", size = 4112827, upload-time = "2026-05-06T20:04:47.982Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/54/39d3c58de992767834120fe3735b85cc60dd00a69b377c3d947ca6f172a1/uv-0.11.11-py3-none-linux_armv6l.whl", hash = "sha256:4977a1193e5dc9c2934b9f97d6cf787382f80deae17646640ee583cfc61486c0", size = 23537936, upload-time = "2026-05-06T20:04:58.626Z" }, + { url = "https://files.pythonhosted.org/packages/de/c9/d2d7ca30abf4c2d5ae0d9360a1e154115af176308ef1ecdc8bf7af724cf8/uv-0.11.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:92817f276758e41b4160fcb6d457ebd9f228f0473efe3808891164f326fdea38", size = 23068282, upload-time = "2026-05-06T20:05:01.466Z" }, + { url = "https://files.pythonhosted.org/packages/fa/37/f64decba47d7afaace3f238aa4a416dca947bd0a1a9b534c3a0f179e1016/uv-0.11.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6eec6ad051e6e5d922cd547b9f7b09a7f821597ae01900a6f01b0a01317e5fd0", size = 21671522, upload-time = "2026-05-06T20:05:04.382Z" }, + { url = "https://files.pythonhosted.org/packages/93/a6/c129878d7c2a66ffdaa12dc253d3135c5e10fc5b5e15812791e188c6dbec/uv-0.11.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:1d227bb53b701e533f0aa074dd145a6fa31492dc7d6d57a6e72a700b9a4a1991", size = 23283200, upload-time = "2026-05-06T20:04:39.879Z" }, + { url = "https://files.pythonhosted.org/packages/8f/c2/cff1f9ab7eda3d863e9866fca0e14df37c0fd734b66ebb77d751258b2fae/uv-0.11.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:05ee9f18701692fcb22db98085c041a3be7a35b88c710dea4487c293f42a4b95", size = 23081561, upload-time = "2026-05-06T20:05:07.149Z" }, + { url = "https://files.pythonhosted.org/packages/ca/44/ebd02ca8fae5961d1bcbcee11019dd170dd0d42517afad753281335700cc/uv-0.11.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0632af539d6a1ee00f58da9e7db32fd99e12187aa67426cb90d871154ab5debb", size = 23105780, upload-time = "2026-05-06T20:04:50.107Z" }, + { url = "https://files.pythonhosted.org/packages/86/f7/0741abcd70591a65f85fc4e8fecd3fb3fb4bdfe50042cccf016714955fd9/uv-0.11.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb3f2715551d2fc9ef44b6cf0918fcc556cd99e9bf6caa1d8a870a4657d2b180", size = 24542681, upload-time = "2026-05-06T20:04:53.014Z" }, + { url = "https://files.pythonhosted.org/packages/b1/42/46e7e35f1f39e39d4bf0f712479768cf8d33eb7f35b67fceaea43e975dfd/uv-0.11.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c86bd6460579857d7e359bdbfe6f688076c654481ae933151d1449f9ea672fb6", size = 25459284, upload-time = "2026-05-06T20:04:34.168Z" }, + { url = "https://files.pythonhosted.org/packages/e8/fc/efdb16e1a6c619b021259ac8d8e4b6afd97efb446054ea28761eb2e1a177/uv-0.11.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0f69f4df007c7506db8d7f77ccabd466a886ac21e9b04a479dd0cd22e26d2262", size = 24560769, upload-time = "2026-05-06T20:04:42.648Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f8/a5d5bac297b1379719050788c6b852c6b3eefcb1e82d8465ed22c10cede7/uv-0.11.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5b9f31dab557b5ee4257d8c6ba2608a63c7278537cb0cd102cf6fc518e3fb5c", size = 24639659, upload-time = "2026-05-06T20:04:31.491Z" }, + { url = "https://files.pythonhosted.org/packages/ee/d5/f3be167a43192062f1409fd6b857a612665d331174293b4ffc73218872e1/uv-0.11.11-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:8e8faf2e5b3517155fd18e509b19b21135247d43b7fb9a8d61a44a53118d5ab7", size = 23388445, upload-time = "2026-05-06T20:04:25.199Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cd/ef1f573ee8edd2beab9fcd2449121483829621b3b57f7ba3f35c56ef373b/uv-0.11.11-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:3f8c9a1bea743a3fe39e956455686f4d0dd25ef58e8d70dc11a45381fd7c50e5", size = 24114301, upload-time = "2026-05-06T20:04:28.586Z" }, + { url = "https://files.pythonhosted.org/packages/9d/be/9181158465719e875a6995c10af24e00cdefba3fe6c9c8cbb02d34b2ade7/uv-0.11.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:f68dc7b62050a26ac6b1491398aebbbf0fa5485627e73b1d626666a097dbab07", size = 24155126, upload-time = "2026-05-06T20:04:55.98Z" }, + { url = "https://files.pythonhosted.org/packages/71/9c/bb306f9964870847f02a931d1fff896726f8bafcf9ce917122ac1bfef14c/uv-0.11.11-py3-none-musllinux_1_1_i686.whl", hash = "sha256:29ddb0d9b24a30ff4360b94e3cb704e82cd5fda86dc224032251f33ab5ceb79e", size = 23824684, upload-time = "2026-05-06T20:05:10.305Z" }, + { url = "https://files.pythonhosted.org/packages/56/48/434a1cf4798ca200e0dcb36411ba38013edb6d3e1aeb4cd85e8a2d7db9ca/uv-0.11.11-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:505a31f2c30fa9e83b1853cab06c5b92e66341c914c6f20f3878903aa09a6f34", size = 24862560, upload-time = "2026-05-06T20:04:37.287Z" }, + { url = "https://files.pythonhosted.org/packages/63/3a/997cddf82917f084d486e1c268c7e94836190fd928c93aa3fb92caee9a7f/uv-0.11.11-py3-none-win32.whl", hash = "sha256:c1e0e3e18cc94680642eac3c3f19f2635c17dd058edcb41b78cbdc459f574eb4", size = 22573619, upload-time = "2026-05-06T20:04:45.35Z" }, + { url = "https://files.pythonhosted.org/packages/30/5f/db34b840f8d86833ef810de8150fc9ce01a03c779393e08eadbcc4c010d5/uv-0.11.11-py3-none-win_amd64.whl", hash = "sha256:36412b13f6287304789abdf40122d268cee548fce3573e07d148a29370181421", size = 25170135, upload-time = "2026-05-06T20:05:13.001Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3e/f3ba2557b437ec5b1fde1e0d5248b723432dc90f09b0050f52695596fd2e/uv-0.11.11-py3-none-win_arm64.whl", hash = "sha256:011f42faf5d267a6681ea77e3f236f275cb4490efeecb9599de74dc7ad7df8f6", size = 23597162, upload-time = "2026-05-06T20:05:16.095Z" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "virtualenv" +version = "21.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ec/0d/915c02c94d207b85580eb09bffab54438a709e7288524094fe781da526c2/virtualenv-21.3.1.tar.gz", hash = "sha256:c2305bc1fddeec40699b8370d13f8d431b0701f00ce895061ce493aeded4426b", size = 7613791, upload-time = "2026-05-05T01:34:31.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/4f/f71e641e504111a5a74e3a20bc52d01bd86788b22699dd3fee1c63253cf6/virtualenv-21.3.1-py3-none-any.whl", hash = "sha256:d1a71cf58f2f9228fff23a1f6ec15d39785c6b32e03658d104974247145edd35", size = 7594539, upload-time = "2026-05-05T01:34:28.98Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "wcmatch" +version = "10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bracex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/3e/c0bdc27cf06f4e47680bd5803a07cb3dfd17de84cde92dd217dcb9e05253/wcmatch-10.1.tar.gz", hash = "sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af", size = 117421, upload-time = "2025-06-22T19:14:02.49Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/d8/0d1d2e9d3fabcf5d6840362adcf05f8cf3cd06a73358140c3a97189238ae/wcmatch-10.1-py3-none-any.whl", hash = "sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a", size = 39854, upload-time = "2025-06-22T19:14:00.978Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, + { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, + { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, + { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, + { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, + { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, + { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] + +[[package]] +name = "xxhash" +version = "3.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/2f/e183a1b407002f5af81822bee18b61cdb94b8670208ef34734d8d2b8ebe9/xxhash-3.7.0.tar.gz", hash = "sha256:6cc4eefbb542a5d6ffd6d70ea9c502957c925e800f998c5630ecc809d6702bae", size = 82022, upload-time = "2026-04-25T11:10:32.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/f4/7bd35089ff1f8e2c96baa2dce05775a122aacd2e3830a73165e27a4d0848/xxhash-3.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fdc7d06929ae28dda98297a18eef7b0fd38991a3b405d8d7b55c9ef24c296958", size = 33423, upload-time = "2026-04-25T11:05:47.628Z" }, + { url = "https://files.pythonhosted.org/packages/a3/26/4e00c88a6a2c8a759cfb77d2a9a405f901e8aa66e60ef1fd0aeb35edda48/xxhash-3.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea6daa712f4e094a30830cf01e9b47d03b24d05cc9dab8609f0d9a9db8454712", size = 30857, upload-time = "2026-04-25T11:05:49.189Z" }, + { url = "https://files.pythonhosted.org/packages/82/2f/eeb942c17a5a761a8f01cb9180a0b76bfb62a2c39e6f46b1f9001899027a/xxhash-3.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9e6c0d843f1daf85ea23aeb053579135552bde575b7b98af20bfc667b6e4548d", size = 194702, upload-time = "2026-04-25T11:05:50.457Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/96f132c08b1e5951c68691d3b9ec351ec2edc028f6a01fcd294f46b9d9f0/xxhash-3.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:363c139bf15e1ac5f136b981d3c077eb551299b1effede7f12faa010b8590a60", size = 213613, upload-time = "2026-04-25T11:05:52.571Z" }, + { url = "https://files.pythonhosted.org/packages/82/89/d4e92b796c5ed052d29ed324dbfc1dc1188e0c4bf64bebbf0f8fc20698df/xxhash-3.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a778b25874cb0f862eaab5986bff4ca49ffb0def7c0a34c237b948b3c6c775b2", size = 236726, upload-time = "2026-04-25T11:05:54.395Z" }, + { url = "https://files.pythonhosted.org/packages/40/f1/81fc4361921dc6e557a9c60cb3712f36d244d06eeeb71cd2f4252ac42678/xxhash-3.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e1860f1e43d40e9d904cf22d93e587ea42e010ebce4160877e46bcab4bc232a", size = 212443, upload-time = "2026-04-25T11:05:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d0/afeddd4cff50a332f50d4b8a2e8857673153ab0564ef472fcdeb0b5430df/xxhash-3.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9122ad6f867c4a0f5e655f5c3bdf89103852009dbb442a3d23e688b9e699e800", size = 445793, upload-time = "2026-04-25T11:05:58.953Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d0/3c91e4e6a05ca4d7df8e39ec3a75b713609258ec84705ab34be6430826a1/xxhash-3.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d9110d0c3fb02679972837a033251fd186c529aa62f19c132fc909c74052b8", size = 193937, upload-time = "2026-04-25T11:06:00.546Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3a/a6b0772d9801dd4bea4ca4fd34734d6e9b51a711c8a611a24a79de26a878/xxhash-3.7.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:347a93f2b4ce67ce61959665e32a7447c380f8347e55e100daa23766baacf0e5", size = 285188, upload-time = "2026-04-25T11:06:01.96Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f8/cf8e31fd7282230fe7367cd501a2e75b4b67b222bfc7eacccfc20d2652cb/xxhash-3.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:acbb48679ddf3852c45280c10ff10d52ca2cd1da2e552fb81db1ff786c75d0e4", size = 210966, upload-time = "2026-04-25T11:06:03.453Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f0/fd36cc4a81bf52ee5633275daae2b93dd958aace67fd4f5d466ec83b5f35/xxhash-3.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:fe14c356f8b23ad811dc026077a6d4abccdaa7bce5ca98579605550657b6fcfb", size = 241994, upload-time = "2026-04-25T11:06:05.264Z" }, + { url = "https://files.pythonhosted.org/packages/08/e1/67f5d9c9369be42eaf99ba02c01bf14c5ecd67087b02567960bfcee43b63/xxhash-3.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f420ad3d41e38194353a498bbc9561fd5a9973a27b536ce46d8583479cf44335", size = 198707, upload-time = "2026-04-25T11:06:07.044Z" }, + { url = "https://files.pythonhosted.org/packages/50/17/a4c865ca22d2da6b1bc7d739bf88cab209533cf52ba06ca9da27c3039bee/xxhash-3.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:693d02c6dc7d1aa0a45921d54cd8c1ff629e09dfdc2238471507af1f7a1c6f04", size = 210917, upload-time = "2026-04-25T11:06:08.853Z" }, + { url = "https://files.pythonhosted.org/packages/49/8b/453b35810d697abac3c96bde3528bece685869227da274eb80a4a4d4a119/xxhash-3.7.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:14bf7a54e43825ec131ee7fe3c60e142e7c2c1e676ad0f93fc893432d15414af", size = 275772, upload-time = "2026-04-25T11:06:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ad/4eed7eab07fd3ee6678f416190f0413d097ab5d7c1278906bf1e9549d789/xxhash-3.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ae3a39a4d96bdb6f8d154fd7f490c4ad06f0532fcd2bb656052a9a7762cf5d31", size = 414068, upload-time = "2026-04-25T11:06:12.511Z" }, + { url = "https://files.pythonhosted.org/packages/d3/4e/fd6f8a680ba248fdb83054fa71a8bfa3891225200de1708b888ef2c49829/xxhash-3.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1cc07c639e3a77ef1d32987464d3e408565b8a3be57b545d3542b191054d9923", size = 191459, upload-time = "2026-04-25T11:06:14.07Z" }, + { url = "https://files.pythonhosted.org/packages/50/7c/8cb34b3bed4f44ca6827a534d50833f9bc6c006e83b0eb410ac9fa0793bd/xxhash-3.7.0-cp311-cp311-win32.whl", hash = "sha256:3281ba1d1e60ee7a382a7b958513ba03c2c0d5fcbd9a6f7517c0a81251a23422", size = 30628, upload-time = "2026-04-25T11:06:15.802Z" }, + { url = "https://files.pythonhosted.org/packages/0b/47/a49767bd7b40782bedae9ff0721bfe1d7e4dd9dc1585dea684e57ba67c20/xxhash-3.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:a7f25baec4c5d851d40718d6fae52285b31683093d4ff5207e63ab306ccf14a5", size = 31461, upload-time = "2026-04-25T11:06:17.104Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c6/3957bfacfb706bd687be246dfa8dd60f8df97c44186d229f7fd6e26c4b7e/xxhash-3.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:4c2454448ce847c72635827bb75c15c5a3434b03ee1afd28cb6dc6fb2597d830", size = 27746, upload-time = "2026-04-25T11:06:18.716Z" }, + { url = "https://files.pythonhosted.org/packages/f2/8a/51a14cdef4728c6c2337db8a7d8704422cc65676d9199d77215464c880af/xxhash-3.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:082c87bfdd2b9f457606c7a4a53457f4c4b48b0cdc48de0277f4349d79bb3d7a", size = 33357, upload-time = "2026-04-25T11:06:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/b9/1b/0c2c933809421ffd9bf42b59315552c143c755db5d9a816b2f1ae273e884/xxhash-3.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5e7ce913b61f35b0c1c839a49ac9c8e75dd8d860150688aed353b0ce1bf409d8", size = 30869, upload-time = "2026-04-25T11:06:21.989Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/89d5fdd6ee12d70ba99451de46dd0e8010167468dcd913ec855653f4dd50/xxhash-3.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3beb1de3b1e9694fcdd853e570ee64c631c7062435d2f8c69c1adf809bc086f0", size = 194100, upload-time = "2026-04-25T11:06:23.586Z" }, + { url = "https://files.pythonhosted.org/packages/87/ee/2f9f2ed993e77206d1e66991290a1ebe22e843351ca3ebec8e49e01ba186/xxhash-3.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3e7b689c3bce16699efcf736066f5c6cc4472c3840fe4b22bd8279daf4abdac", size = 212977, upload-time = "2026-04-25T11:06:25.019Z" }, + { url = "https://files.pythonhosted.org/packages/de/60/5a91644615a9e9d4e42c2e9925f1908e3a24e4e691d9de7340d565bea024/xxhash-3.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a6545e6b409e3d5cbafc850fb84c55a1ca26ed15a6b11e3bf07a0e0cd84517c8", size = 236373, upload-time = "2026-04-25T11:06:26.482Z" }, + { url = "https://files.pythonhosted.org/packages/22/c0/f3a9384eaaed9d14d4d062a5d953aa0da489bfe9747877aa994caa87cd0b/xxhash-3.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:31ab1461c77a11461d703c88eb949e132a1c6515933cf675d97ec680f4bd18de", size = 212229, upload-time = "2026-04-25T11:06:28.065Z" }, + { url = "https://files.pythonhosted.org/packages/2e/67/02f07a9fd79726804190f2172c4894c3ed9a4ebccaca05653c84beb58025/xxhash-3.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c4d596b7676f811172687ec567cbafb9e4dea2f9be1bbb4f622410cb7f40f40", size = 445462, upload-time = "2026-04-25T11:06:30.048Z" }, + { url = "https://files.pythonhosted.org/packages/40/37/558f5a90c0672fc9b4402dc25d87ac5b7406616e8969430c9ca4e52ee74d/xxhash-3.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13805f0461cba0a857924e70ff91ae6d52d2598f79a884e788db80532614a4a1", size = 193932, upload-time = "2026-04-25T11:06:31.857Z" }, + { url = "https://files.pythonhosted.org/packages/d5/90/aaa09cd58661d32044dbbad7df55bbe22a623032b810e7ed3b8c569a2a6f/xxhash-3.7.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d398f372496152f1c6933a33566373f8d1b37b98b8c9d608fa6edc0976f23b2", size = 284807, upload-time = "2026-04-25T11:06:33.697Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f3/53df3719ab127a02c174f0c1c74924fcd110866e89c966bc7909cfa8fa84/xxhash-3.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d610aa62cdb7d4d497740741772a24a794903bf3e79eaa51d2e800082abe11e5", size = 210445, upload-time = "2026-04-25T11:06:35.488Z" }, + { url = "https://files.pythonhosted.org/packages/72/33/d219975c0e8b6fa2eb9ccd486fe47e21bf1847985b878dd2fbc3126e0d5c/xxhash-3.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:073c23900a9fbf3d26616c17c830db28af9803677cd5b33aea3224d824111514", size = 241273, upload-time = "2026-04-25T11:06:37.24Z" }, + { url = "https://files.pythonhosted.org/packages/3e/50/49b1afe610eb3964cedcb90a4d4c3d46a261ee8669cbd4f060652619ae3c/xxhash-3.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:418a463c3e6a590c0cdc890f8be19adb44a8c8acd175ca5b2a6de77e61d0b386", size = 197950, upload-time = "2026-04-25T11:06:39.148Z" }, + { url = "https://files.pythonhosted.org/packages/c6/75/5f42a1a4c78717d906a4b6a140c6dbf837ab1f547a54d23c4e2903310936/xxhash-3.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:03f8ff4474ee61c845758ce00711d7087a770d77efb36f7e74a6e867301000b8", size = 210709, upload-time = "2026-04-25T11:06:40.958Z" }, + { url = "https://files.pythonhosted.org/packages/8a/85/237e446c25abced71e9c53d269f2cef5bab8a82b3f88a12e00c5368e7368/xxhash-3.7.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:44fba4a5f1d179b7ddc7b3dc40f56f9209046421679b57025d4d8821b376fd8d", size = 275345, upload-time = "2026-04-25T11:06:42.525Z" }, + { url = "https://files.pythonhosted.org/packages/62/34/c2c26c0a6a9cc739bc2a5f0ae03ba8b87deb12b8bce35f7ac495e790dc6d/xxhash-3.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31e3516a0f829d06ded4a2c0f3c7c5561993256bfa1c493975fb9dc7bfa828a1", size = 414056, upload-time = "2026-04-25T11:06:44.343Z" }, + { url = "https://files.pythonhosted.org/packages/a0/aa/5c58e9bc8071b8afd8dcf297ff362f723c4892168faba149f19904132bf4/xxhash-3.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b59ee2ac81de57771a09ecad09191e840a1d2fae1ef684208320591055768f83", size = 191485, upload-time = "2026-04-25T11:06:46.262Z" }, + { url = "https://files.pythonhosted.org/packages/d4/69/a929cf9d1e2e65a48b818cdce72cb6b69eab2e6877f21436d0a1942aff43/xxhash-3.7.0-cp312-cp312-win32.whl", hash = "sha256:74bbd92f8c7fcc397ba0a11bfdc106bc72ad7f11e3a60277753f87e7532b4d81", size = 30671, upload-time = "2026-04-25T11:06:48.039Z" }, + { url = "https://files.pythonhosted.org/packages/b9/1b/104b41a8947f4e1d4a66ce1e628eea752f37d1890bfd7453559ca7a3d950/xxhash-3.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:7bd7bc82dd4f185f28f35193c2e968ef46131628e3cac62f639dadf321cba4d1", size = 31514, upload-time = "2026-04-25T11:06:49.279Z" }, + { url = "https://files.pythonhosted.org/packages/98/a0/1fd0ea1f1b886d9e7c73f0397571e22333a7d79e31da6d7127c2a4a71d75/xxhash-3.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:7d7148180ec99ba36585b42c8c5de25e9b40191613bc4be68909b4d25a77a852", size = 27761, upload-time = "2026-04-25T11:06:50.448Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ca/d5174b4c36d10f64d4ca7050563138c5a599efb01a765858ddefc9c1202a/xxhash-3.7.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:4b6d6b33f141158692bd4eafbb96edbc5aa0dabdb593a962db01a91983d4f8fa", size = 36813, upload-time = "2026-04-25T11:06:51.73Z" }, + { url = "https://files.pythonhosted.org/packages/41/d0/abc6c9d347ba1f1e1e1d98125d0881a0452c7f9a76a9dd03a7b5d2197f23/xxhash-3.7.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:845d347df254d6c619f616afa921331bada8614b8d373d58725c663ba97c3605", size = 35121, upload-time = "2026-04-25T11:06:53.048Z" }, + { url = "https://files.pythonhosted.org/packages/bf/11/4cc834eb3d79f2f2b3a6ef7324195208bcdfbdcf7534d2b17267aa5f3a8f/xxhash-3.7.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:fddbbb69a6fff4f421e7a0d1fa28f894b20112e9e3fab306af451e2dfd0e459b", size = 29624, upload-time = "2026-04-25T11:06:54.311Z" }, + { url = "https://files.pythonhosted.org/packages/23/83/e97d3e7b635fe73a1dfb1e91f805324dd6d930bb42041cbf18f183bc0b6d/xxhash-3.7.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:54876a4e45101cec2bf8f31a973cda073a23e2e108538dad224ba07f85f22487", size = 30638, upload-time = "2026-04-25T11:06:55.864Z" }, + { url = "https://files.pythonhosted.org/packages/f4/40/d84951d80c35db1f4c40a29a64a8520eea5d56e764c603906b4fe763580f/xxhash-3.7.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:0c72fe9c7e3d6dfd7f1e21e224a877917fa09c465694ba4e06464b9511b65544", size = 33323, upload-time = "2026-04-25T11:06:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/c7dc6558d97e9ab023f663d69ab28b340ed9bf4d2d94f2c259cf896bb354/xxhash-3.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a6d73a830b17ef49bc04e00182bd839164c1b3c59c127cd7c54fcb10c7ed8ee8", size = 33362, upload-time = "2026-04-25T11:06:58.656Z" }, + { url = "https://files.pythonhosted.org/packages/2a/6e/46b84017b1301d54091430353d4ad5901654a3e0871649877a416f7f1644/xxhash-3.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:91c3b07cf3362086d8f126c6aecd8e5e9396ad8b2f2219ea7e49a8250c318acd", size = 30874, upload-time = "2026-04-25T11:06:59.834Z" }, + { url = "https://files.pythonhosted.org/packages/df/5e/8f9158e3ab906ad3fec51e09b5ea0093e769f12207bfa42a368ca204e7ab/xxhash-3.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:50e879ebbac351c81565ca108db766d7832f5b8b6a5b14b8c0151f7190028e3d", size = 194185, upload-time = "2026-04-25T11:07:01.658Z" }, + { url = "https://files.pythonhosted.org/packages/f3/29/a804ded9f5d3d3758292678d23e7528b08fda7b7e750688d08b052322475/xxhash-3.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:921c14e93817842dd0dd9f372890a0f0c72e534650b6ab13c5be5cd0db11d47e", size = 213033, upload-time = "2026-04-25T11:07:03.606Z" }, + { url = "https://files.pythonhosted.org/packages/8b/91/1ce5a7d2fdc975267320e2c78fc1cecfe7ab735ccbcf6993ec5dd541cb2c/xxhash-3.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e64a7c9d7dfca3e0fafcbc5e455519090706a3e36e95d655cec3e04e79f95aaa", size = 236140, upload-time = "2026-04-25T11:07:05.396Z" }, + { url = "https://files.pythonhosted.org/packages/34/04/fd595a4fd8617b05fa27bd9b684ecb4985bfed27917848eea85d54036d06/xxhash-3.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2220af08163baf5fa36c2b8af079dc2cbe6e66ae061385267f9472362dfd53c6", size = 212291, upload-time = "2026-04-25T11:07:06.966Z" }, + { url = "https://files.pythonhosted.org/packages/03/fb/f1a379cbc372ae5b9f4ab36154c48a849ca6ebe3ac477067a57865bf3bc6/xxhash-3.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f14bb8b22a4a91325813e3d553b8963c10cf8c756cff65ee50c194431296c655", size = 445532, upload-time = "2026-04-25T11:07:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/65/59/172424b79f8cfd4b6d8a122b2193e6b8ad4b11f7159bb3b6f9b3191329bb/xxhash-3.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:496736f86a9bedaf64b0dc70e3539d0766df01c71ea22032698e88f3f04a1ce9", size = 193990, upload-time = "2026-04-25T11:07:10.315Z" }, + { url = "https://files.pythonhosted.org/packages/b9/19/aeac22161d953f139f07ba5586cb4a17c5b7b6dff985122803bb12933500/xxhash-3.7.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0ff71596bd79816975b3de7130ab1ff4541410285a3c084584eeb1c8239996fd", size = 284876, upload-time = "2026-04-25T11:07:12.15Z" }, + { url = "https://files.pythonhosted.org/packages/77/d5/4fd0b59e7a02242953da05ff679fbb961b0a4368eac97a217e11dae110c1/xxhash-3.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1ad86695c19b1d46fe106925db3c7a37f16be37669dcf58dcc70a9dd6e324676", size = 210495, upload-time = "2026-04-25T11:07:13.952Z" }, + { url = "https://files.pythonhosted.org/packages/aa/fb/976a3165c728c7faf74aa1b5ab3cf6a85e6d731612894741840524c7d28c/xxhash-3.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:970f9f8c50961d639cbd0d988c96f80ddf66006de93641719282c4fe7a87c5e6", size = 241331, upload-time = "2026-04-25T11:07:15.557Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2c/6763d5901d53ac9e6ba296e5717ae599025c9d268396e8faa8b4b0a8e0ac/xxhash-3.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5886ad85e9e347911783760a1d16cb6b393e8f9e3b52c982568226cb56927bdc", size = 198037, upload-time = "2026-04-25T11:07:17.563Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/876e722d533833f5f9a83473e6ba993e48745701096944e77bbecf29b2c3/xxhash-3.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6e934bbae1e0ec74e27d5f0d7f37ef547ce5ff9f0a7e63fb39e559fc99526734", size = 210744, upload-time = "2026-04-25T11:07:19.055Z" }, + { url = "https://files.pythonhosted.org/packages/21/e6/d7e7baef7ce24166b4668d3c48557bb35a23b92ecadcac7e7718d099ab69/xxhash-3.7.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:3b6b3d28228af044ebcded71c4a3dd86e1dbd7e2f4645bf40f7b5da65bb5fb5a", size = 275406, upload-time = "2026-04-25T11:07:20.908Z" }, + { url = "https://files.pythonhosted.org/packages/92/fe/198b3763b2e01ca908f2154969a2352ec99bda892b574a11a9a151c5ede4/xxhash-3.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:6be4d70d9ab76c9f324ead9c01af6ff52c324745ea0c3731682a0cf99720f1fe", size = 414125, upload-time = "2026-04-25T11:07:23.037Z" }, + { url = "https://files.pythonhosted.org/packages/3a/6d/019a11affd5a5499137cacca53808659964785439855b5aa40dfd3412916/xxhash-3.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:151d7520838d4465461a0b7f4ae488b3b00de16183dd3214c1a6b14bf89d7fb6", size = 191555, upload-time = "2026-04-25T11:07:24.991Z" }, + { url = "https://files.pythonhosted.org/packages/76/21/b96d58568df2d01533244c3e0e5cbdd0c8b2b25c4bec4d72f19259a292d7/xxhash-3.7.0-cp313-cp313-win32.whl", hash = "sha256:d798c1e291bffb8e37b5bbe0dda77fc767cd19e89cadaf66e6ed5d0ff88c9fe6", size = 30668, upload-time = "2026-04-25T11:07:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/99/57/d849a8d3afa1f8f4bc6a831cd89f49f9706fbbad94d2975d6140a171988c/xxhash-3.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:875811ba23c543b1a1c3143c926e43996eb27ebb8f52d3500744aa608c275aed", size = 31524, upload-time = "2026-04-25T11:07:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/81/52/bacc753e92dee78b058af8dcef0a50815f5f860986c664a92d75f965b6a5/xxhash-3.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:54a675cb300dda83d71daae2a599389d22db8021a0f8db0dd659e14626eb3ecc", size = 27768, upload-time = "2026-04-25T11:07:29.113Z" }, + { url = "https://files.pythonhosted.org/packages/1c/47/ddbd683b7fc7e592c1a8d9d65f73ce9ab513f082b3967eee2baf549b8fc6/xxhash-3.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a3b19a42111c4057c1547a4a1396a53961dca576a0f6b82bfa88a2d1561764b2", size = 33576, upload-time = "2026-04-25T11:07:30.469Z" }, + { url = "https://files.pythonhosted.org/packages/07/f2/36d3310161db7f72efb4562aadde0ed429f1d0531782dd6345b12d2da527/xxhash-3.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8f4608a06e4d61b7a3425665a46d00e0579122e1a2fae97a0c52953a3aad9aa3", size = 31123, upload-time = "2026-04-25T11:07:31.989Z" }, + { url = "https://files.pythonhosted.org/packages/0d/3f/75937a5c69556ed213021e43cbedd84c8e0279d0d74e7d41a255d84ba4b1/xxhash-3.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ad37c7792479e49cf96c1ab25517d7003fe0d93687a772ba19a097d235bbe41e", size = 196491, upload-time = "2026-04-25T11:07:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/f10d7ff8c7a733d4403a43b9de18c8fabc005f98cec054644f04418659ee/xxhash-3.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc026e3b89d98e30a8288c95cb696e77d150b3f0fb7a51f73dcd49ee6b5577fa", size = 215793, upload-time = "2026-04-25T11:07:34.919Z" }, + { url = "https://files.pythonhosted.org/packages/8b/fd/778f60aa295f58907938f030a8b514611f391405614a525cccd2ffc00eb5/xxhash-3.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c9b31ab1f28b078a6a1ac1a54eb35e7d5390deddd56870d0be3a0a733d1c321c", size = 237993, upload-time = "2026-04-25T11:07:36.638Z" }, + { url = "https://files.pythonhosted.org/packages/70/f5/736db5de387b4a540e37a05b84b40dc58a1ce974bfd2b4e5754ce29b68c3/xxhash-3.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3bb5fd680c038fd5229e44e9c493782f90df9bef632fd0499d442374688ff70b", size = 214887, upload-time = "2026-04-25T11:07:38.564Z" }, + { url = "https://files.pythonhosted.org/packages/4d/aa/09a095f22fdb9a27fbb716841fbff52119721f9ca4261952d07a912f7839/xxhash-3.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:030c0fd688fce3569fbb49a2feefd4110cbb0b650186fb4610759ecfac677548", size = 448407, upload-time = "2026-04-25T11:07:40.552Z" }, + { url = "https://files.pythonhosted.org/packages/74/8a/b745efeeca9e34a91c26fdc97ad8514c43d5a81ac78565cba80a1353870a/xxhash-3.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b1bde10324f4c31812ae0d0502e92d916ae8917cad7209353f122b8b8f610c3", size = 196119, upload-time = "2026-04-25T11:07:42.101Z" }, + { url = "https://files.pythonhosted.org/packages/8a/5c/0cfceb024af90c191f665c7933b1f318ee234f4797858383bebd1881d52f/xxhash-3.7.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:503722d52a615f2604f5e7611de7d43878df010dc0053094ef91cb9a9ac3d987", size = 286751, upload-time = "2026-04-25T11:07:43.568Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0a/0793e405dc3cf8f4ebe2c1acec1e4e4608cd9e7e50ea691dabbc2a95ccbb/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c72500a3b6d6c30ebfc135035bcace9eb5884f2dc220804efcaaba43e9f611dd", size = 212961, upload-time = "2026-04-25T11:07:45.388Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7e/721118ffc63bfff94aa565bcf2555a820f9f4bdb0f001e0d609bdfad70de/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:43475925a766d01ca8cd9a857fd87f3d50406983c8506a4c07c4df12adcc867f", size = 243703, upload-time = "2026-04-25T11:07:47.053Z" }, + { url = "https://files.pythonhosted.org/packages/6e/18/16f6267160488b8276fd3d449d425712512add292ba545c1b6946bfdb7dd/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8d09dfd2ab135b985daf868b594315ebe11ad86cd9fea46e6c69f19b28f7d25a", size = 200894, upload-time = "2026-04-25T11:07:48.657Z" }, + { url = "https://files.pythonhosted.org/packages/2d/94/80ba841287fd97e3e9cac1d228788c8ef623746f570404961eec748ecb5c/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c50269d0055ac1faecfd559886d2cbe4b730de236585aba0e873f9d9dadbe585", size = 213357, upload-time = "2026-04-25T11:07:50.257Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7e/106d4067130c59f1e18a55ffadcd876d8c68534883a1e02685b29d3d8153/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:1910df4756a5ab58cfad8744fc2d0f23926e3efcc346ee76e87b974abab922f4", size = 277600, upload-time = "2026-04-25T11:07:51.745Z" }, + { url = "https://files.pythonhosted.org/packages/c5/86/a081dd30da71d720b2612a792bfd55e45fa9a07ac76a0507f60487473c25/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d006faf3b491957efcb433489be3c149efe4787b7063d5cddb8ddaefdc60e0c1", size = 416980, upload-time = "2026-04-25T11:07:53.504Z" }, + { url = "https://files.pythonhosted.org/packages/35/29/1a95221a029a3c1293773869e1ab47b07cbbdd82444a42809e8c60156626/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:abb65b4e947e958f7b3b0d71db3ce447d1bc5f37f5eab871ce7223bda8768a04", size = 193840, upload-time = "2026-04-25T11:07:55.103Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/db909dd0823285de2286f67e10ee4d81e96ad35d7d8e964ecb07fccd8af9/xxhash-3.7.0-cp313-cp313t-win32.whl", hash = "sha256:178959906cb1716a1ce08e0d69c82886c70a15a6f2790fc084fdd146ca30cd49", size = 30966, upload-time = "2026-04-25T11:07:56.524Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ff/d705b15b22f21ee106adce239cb65d35067a158c630b240270f09b17c2e6/xxhash-3.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2524a1e20d4c231d13b50f7cf39e44265b055669a64a7a4b9a2a44faa03f19b6", size = 31784, upload-time = "2026-04-25T11:07:57.758Z" }, + { url = "https://files.pythonhosted.org/packages/a2/1f/b2cf83c3638fd0588e0b17f22e5a9400bdfb1a3e3755324ac0aee2250b88/xxhash-3.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:37d994d0ffe81ef087bb330d392caa809bb5853c77e22ea3f71db024a0543dba", size = 27932, upload-time = "2026-04-25T11:07:59.109Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cc/431db584f6fbb9312e40a173af027644e5580d39df1f73603cbb9dca4d6b/xxhash-3.7.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:8c5fcfd806c335bfa2adf1cd0b3110a44fc7b6995c3a648c27489bae85801465", size = 36644, upload-time = "2026-04-25T11:08:00.658Z" }, + { url = "https://files.pythonhosted.org/packages/bc/01/255ec513e0a705d1f9a61413e78dfce4e3235203f0ed525a24c2b4b56345/xxhash-3.7.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:506a0b488f190f0a06769575e30caf71615c898ed93ab18b0dbcb6dec5c3713c", size = 35003, upload-time = "2026-04-25T11:08:02.338Z" }, + { url = "https://files.pythonhosted.org/packages/68/70/c55fc33c93445b44d8fc5a17b41ed99e3cebe92bcf8396809e63fc9a1165/xxhash-3.7.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:ec68dbba21532c0173a9872298e65c89749f7c9d21538c3a78b5bb6105871568", size = 29655, upload-time = "2026-04-25T11:08:03.701Z" }, + { url = "https://files.pythonhosted.org/packages/c2/72/ff8de73df000d74467d12a59ce6d6e2b2a368b978d41ab7b1fba5ed442be/xxhash-3.7.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:fa77e7ec1450d415d20129961814787c9abd9a07f98872f070b1fe96c5084611", size = 30664, upload-time = "2026-04-25T11:08:05.011Z" }, + { url = "https://files.pythonhosted.org/packages/b6/91/08416d9bd9bc3bf39d831abe8a5631ac2db5141dfd6fe81c3fe59a1f9264/xxhash-3.7.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:fe32736295ea38e43e7d9424053c8c47c9f64fecfc7c895fb3da9b30b131c9ee", size = 33317, upload-time = "2026-04-25T11:08:06.413Z" }, + { url = "https://files.pythonhosted.org/packages/0e/3b/86b1caa4dee10a99f4bf9521e623359341c5e50d05158fa10c275b2bd079/xxhash-3.7.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ab9dd2c83c4bbd63e422181a76f13502d049d3ddcac9a1bdc29196263d692bb8", size = 33457, upload-time = "2026-04-25T11:08:08.099Z" }, + { url = "https://files.pythonhosted.org/packages/ed/38/98ea14ad1517e1461292a65906951458d520689782bfbae111050145bdba/xxhash-3.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3afec3a336a2286601a437cb07562ab0227685e6fbb9ec17e8c18457ff348ecf", size = 30894, upload-time = "2026-04-25T11:08:09.429Z" }, + { url = "https://files.pythonhosted.org/packages/61/a2/074654d0b893606541199993c7db70067d9fc63b748e0d60020a52a1bd36/xxhash-3.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:565df64437a9390f84465dcca33e7377114c7ede8d05cd2cf20081f831ea788e", size = 194409, upload-time = "2026-04-25T11:08:10.91Z" }, + { url = "https://files.pythonhosted.org/packages/e2/26/6d2a1afc468189f77ca28c32e1c83e1b9da1178231e05641dbc1b350e332/xxhash-3.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12eca820a5d558633d423bf8bb78ce72a55394823f64089247f788a7e0ae691e", size = 213135, upload-time = "2026-04-25T11:08:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0e/d8aecf95e09c42547453137be74d2f7b8b14e08f5177fa2fab6144a19061/xxhash-3.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f262b8f7599516567e070abf607b9af649052b2c4bd6f9be02b0cb41b7024805", size = 236379, upload-time = "2026-04-25T11:08:14.206Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/8140e8210536b3dd0cc816c4faaeb5ba6e63e8125ab25af4bcddd6a037b3/xxhash-3.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1598916cb197681e03e601901e4ab96a9a963de398c59d0964f8a6f44a2b361", size = 212447, upload-time = "2026-04-25T11:08:15.79Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/462001d2903b4bee5a5689598a0a55e5e7cd1ac7f4247a5545cff10d3ebb/xxhash-3.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:322b2f0622230f526aeb1738149948a7ae357a9e2ceb1383c6fd1fdaecdafa16", size = 445660, upload-time = "2026-04-25T11:08:17.441Z" }, + { url = "https://files.pythonhosted.org/packages/23/09/2bd1ed7f8689b20e51727952cac8329d50c694dc32b2eba06ba5bc742b37/xxhash-3.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cc22070880cc57b830a65cde4e65fa884c6d9b28ae4803b5ee05911e7bafba", size = 194076, upload-time = "2026-04-25T11:08:19.134Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6e/692302cd0a5f4ac4e6289f37fa888dc2e1e07750b68fe3e4bfe939b8cea3/xxhash-3.7.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb5a888a968b2434abf9ecda357b5d43f10d7b5a6da6fdbbe036208473aff0e2", size = 284990, upload-time = "2026-04-25T11:08:20.618Z" }, + { url = "https://files.pythonhosted.org/packages/05/d9/e54b159b3d9df7999d2a7c676ce7b323d1b5588a64f8f51ed8172567bd87/xxhash-3.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a999771ff97bec27d18341be4f3a36b163bb1ac41ec17bef6d2dabd84acd33c7", size = 210590, upload-time = "2026-04-25T11:08:22.24Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/0e0df1a3a196ced4ca71de76d65ead25d8e87bbfb87b64306ea47a40c00d/xxhash-3.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ed4a6efe2dee1655adb73e7ad40c6aa955a6892422b1e3b95de6a34de56e3cbb", size = 241442, upload-time = "2026-04-25T11:08:23.844Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a9/d917a7a814e90b218f8a0d37967105eea91bf752c3303683c99a1f7bfc1f/xxhash-3.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9fd17f14ac0faa12126c2f9ca774a8cf342957265ec3c8669c144e5e6cdb478c", size = 198356, upload-time = "2026-04-25T11:08:25.99Z" }, + { url = "https://files.pythonhosted.org/packages/89/5e/f2ba1877c39469abbefc72991d6ebdcbd4c0880db01ae8cb1f553b0c537d/xxhash-3.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:05fd1254268c59b5cb2a029dfc204275e9fc52de2913f1e53aa8d01442c96b4d", size = 210898, upload-time = "2026-04-25T11:08:27.608Z" }, + { url = "https://files.pythonhosted.org/packages/90/c6/be56b58e73de531f39a10de1355bb77ceb663900dc4bf2d6d3002a9c3f9e/xxhash-3.7.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a2eae53197c6276d5b317f75a1be226bbf440c20b58bf525f36b5d0e1f657ca6", size = 275519, upload-time = "2026-04-25T11:08:29.301Z" }, + { url = "https://files.pythonhosted.org/packages/92/e2/17ddc85d5765b9c709f192009ed8f5a1fc876f4eb35bba7c307b5b1169f9/xxhash-3.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:bfe6f92e3522dcbe8c4281efd74fa7542a336cb00b0e3272c4ec0edabeaeaf67", size = 414191, upload-time = "2026-04-25T11:08:31.16Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/85f5b79f4bf1ec7ba052491164adfd4f4e9519f5dc7246de4fbd64a1bd56/xxhash-3.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7ab9a49c410d8c6c786ab99e79c529938d894c01433130353dd0fe999111077a", size = 191604, upload-time = "2026-04-25T11:08:32.862Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d0/6127b623aa4cca18d8b7743592b048d689fd6c6e37ff26a22cddf6cd9d7f/xxhash-3.7.0-cp314-cp314-win32.whl", hash = "sha256:040ea63668f9185b92bc74942df09c7e65703deed71431333678fc6e739a9955", size = 31271, upload-time = "2026-04-25T11:08:34.651Z" }, + { url = "https://files.pythonhosted.org/packages/64/4f/44fc4788568004c43921701cbc127f48218a1eede2c9aea231115323564d/xxhash-3.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2a61e2a3fb23c892496d587b470dee7fa1b58b248a187719c65ea8e94ec13257", size = 32284, upload-time = "2026-04-25T11:08:35.987Z" }, + { url = "https://files.pythonhosted.org/packages/6d/77/18bb895eb60a49453d16e17d67990e5caff557c78eafc90ad4e2eabf4570/xxhash-3.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:c7741c7524961d8c0cb4d4c21b28957ff731a3fd5b5cd8b856dc80a40e9e5acc", size = 28701, upload-time = "2026-04-25T11:08:37.767Z" }, + { url = "https://files.pythonhosted.org/packages/45/a0/46f72244570c550fbbb7db1ef554183dd5ebe9136385f30e032b781ae8f6/xxhash-3.7.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:fc84bf7aa7592f31ec63a3e7b11d624f468a3f19f5238cec7282a42e838ab1d7", size = 33646, upload-time = "2026-04-25T11:08:39.109Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3a/453846a7eceea11e75def361eed01ec6a0205b9822c19927ed364ccae7cc/xxhash-3.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9f1563fdc8abfc389748e6932c7e4e99c89a53e4ec37d4563c24fc06f5e5644b", size = 31125, upload-time = "2026-04-25T11:08:40.467Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3e/49434aba738885d512f9e486db1bdd19db28dfa40372b56da26ef7a4e738/xxhash-3.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d415f18becf6f153046ab6adc97da77e3643a0ee205dae61c4012604113a020", size = 196633, upload-time = "2026-04-25T11:08:41.943Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e9/006cb6127baeb9f8abe6d15e62faa01349f09b34e2bfd65175b2422d026b/xxhash-3.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bb16aa13ed175bc9be5c2491ba031b85a9b51c4ed90e0b3d4ebe63cf3fb54f8e", size = 215899, upload-time = "2026-04-25T11:08:43.645Z" }, + { url = "https://files.pythonhosted.org/packages/27/e4/cc57d72e66df0ae29b914335f1c6dcf61e8f3746ddf0ae3c471aa4f15e00/xxhash-3.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f9fd595f1e5941b3d7863e4774e4b30caa6731fc34b9277da032295aa5656ee5", size = 238116, upload-time = "2026-04-25T11:08:45.698Z" }, + { url = "https://files.pythonhosted.org/packages/af/78/3531d4a3fd8a0038cc6be1f265a69c1b3587f557a10b677dd736de2202c1/xxhash-3.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1295325c5a98d552333fa53dc2b026b0ef0ec9c8e73ca3a952990b4c7d65d459", size = 215012, upload-time = "2026-04-25T11:08:47.355Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f6/259fb1eaaec921f59b17203b0daee69829761226d3b980d5191d7723dd83/xxhash-3.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3573a651d146912da9daa9e29e5fbc45994420daaa9ef1e2fa5823e1dc485513", size = 448534, upload-time = "2026-04-25T11:08:49.149Z" }, + { url = "https://files.pythonhosted.org/packages/7b/16/a66d0eaf6a7e68532c07714361ddc904c663ec940f3b028c1ae4a21a7b9d/xxhash-3.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ec1e080a3d02d94ea9335bfab0e3374b877e25411422c18f51a943fa4b46381", size = 196217, upload-time = "2026-04-25T11:08:50.805Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ef/d2efc7fc51756dc52509109d1a25cefc859d74bc4b19a167b12dbd8c2786/xxhash-3.7.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84415265192072d8638a3afc3c1bc5995e310570cd9acb54dc46d3939e364fe0", size = 286906, upload-time = "2026-04-25T11:08:52.418Z" }, + { url = "https://files.pythonhosted.org/packages/fc/67/25decd1d4a4018582ec4db2a868a2b7e40640f4adb20dfeb19ac923aa825/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d4dea659b57443989ef32f4295104fd6912c73d0bf26d1d148bb88a9f159b02", size = 213057, upload-time = "2026-04-25T11:08:54.105Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5d/17651eb29d06786cdc40c60ae3d27d645aa5d61d2eca6237a7ba0b94789b/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:05ece0fe4d9c9c2728912d1981ae1566cfc83a011571b24732cbf76e1fb70dca", size = 243886, upload-time = "2026-04-25T11:08:56.109Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d4/174d9cf7502243d586e6a9ae842b1ae23026620995114f85f1380e588bc9/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fd880353cf1ffaf321bc18dd663e111976dbd0d3bbd8a66d58d2b470dfa7f396", size = 201015, upload-time = "2026-04-25T11:08:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/91/8c/2254e2d06c3ac5e6fe22eaf3da791b87ea823ae9f2c17b4af66755c5752d/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4e15cc9e2817f6481160f930c62842b3ff419e20e13072bcbab12230943092bc", size = 213457, upload-time = "2026-04-25T11:08:59.826Z" }, + { url = "https://files.pythonhosted.org/packages/79/a2/e3daa762545921173e3360f3b4ff7fc63c2d27359f7230ec1a7a74e117f6/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:90b9d1a8bd37d768ffc92a1f651ec69afc532a96fa1ac2ea7abbed5d630b3237", size = 277738, upload-time = "2026-04-25T11:09:01.423Z" }, + { url = "https://files.pythonhosted.org/packages/e1/4c/e186da2c46b87f5204640e008d42730bf3c1ee9f0efb71ae1ebcdfeac681/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:157c49475b34ecea8809e51123d9769a534e139d1247942f7a4bc67710bb2533", size = 417127, upload-time = "2026-04-25T11:09:03.592Z" }, + { url = "https://files.pythonhosted.org/packages/17/28/3798e15007a3712d0da3d3fe70f8e11916569858b5cc371053bc26270832/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5a6ddec83325685e729ca119d1f5c518ec39294212ecd770e60693cdc5f7eb79", size = 193962, upload-time = "2026-04-25T11:09:06.228Z" }, + { url = "https://files.pythonhosted.org/packages/ad/95/a26baa93b5241fd7630998816a4ec47a5a0bad193b3f8fc8f3593e1a4a67/xxhash-3.7.0-cp314-cp314t-win32.whl", hash = "sha256:a04a6cab47e2166435aaf5b9e5ee41d1532cc8300efdef87f2a4d0acb7db19ed", size = 31643, upload-time = "2026-04-25T11:09:08.153Z" }, + { url = "https://files.pythonhosted.org/packages/44/36/5454f13c447e395f9b06a3e91274c59f503d31fad84e1836efe3bdb71f6a/xxhash-3.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8653dd7c2eda020545bb2c71c7f7039b53fe7434d0fc1a0a9deb79ab3f1a4fc1", size = 32522, upload-time = "2026-04-25T11:09:09.534Z" }, + { url = "https://files.pythonhosted.org/packages/74/35/698e7e3ff38e22992ea24870a511d8762474fb6783627a2910ff22a185c2/xxhash-3.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:468f0fc114faaa4b36699f8e328bbc3bb11dc418ba94ac52c26dd736d4b6c637", size = 28807, upload-time = "2026-04-25T11:09:11.234Z" }, + { url = "https://files.pythonhosted.org/packages/54/c1/e57ac7317b1f58a92bab692da6d497e2a7ce44735b224e296347a7ecc754/xxhash-3.7.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ad3aa71e12ee634f22b39a0ff439357583706e50765f17f05550f92dbf128a23", size = 31232, upload-time = "2026-04-25T11:10:21.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4e/075559bd712bc62e84915ea46bbee859f935d285659082c129bdbff679dd/xxhash-3.7.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5de686e73690cdaf72b96d4fa083c230ec9020bcc2627ce6316138e2cf2fe2d1", size = 28553, upload-time = "2026-04-25T11:10:23.1Z" }, + { url = "https://files.pythonhosted.org/packages/92/ca/a9c78cb384d4b033b0c58196bd5c8509873cabe76389e195127b0302a741/xxhash-3.7.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7fbec49f5341bbdea0c471f7d1e2fb41ae8925af9b6f28025c28defd8eb94274", size = 41109, upload-time = "2026-04-25T11:10:25.022Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b1/dfe2629f7c77eb2fa234c72ff537cdd64939763df704e256446ed364a16d/xxhash-3.7.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48b542c347c2089f43dc5a6db31d2a6f3cdb04ee33505ec6e9f653834dbb0bde", size = 36307, upload-time = "2026-04-25T11:10:26.949Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f7/5a484afce0f48dd8083208b42e4911f290a82c7b52458ef2927e4d421a45/xxhash-3.7.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a169a036bed0995e090d1493b283cc2cc8a6f5046821086b843abefff80643bc", size = 32534, upload-time = "2026-04-25T11:10:29.01Z" }, + { url = "https://files.pythonhosted.org/packages/0f/5f/4acfcd490db9780cf36c58534d828003c564cde5350220a1c783c4d10776/xxhash-3.7.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ec101643395d7f21405b640f728f6f627e6986557027d740f2f9b220955edafe", size = 31552, upload-time = "2026-04-25T11:10:30.727Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/21/093488dfc7cc8964ded15ab726fad40f25fd3d788fd741cc1c5a17d78ee8/zipp-3.23.1.tar.gz", hash = "sha256:32120e378d32cd9714ad503c1d024619063ec28aad2248dc6672ad13edfa5110", size = 25965, upload-time = "2026-04-13T23:21:46.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/8a/0861bec20485572fbddf3dfba2910e38fe249796cb73ecdeb74e07eeb8d3/zipp-3.23.1-py3-none-any.whl", hash = "sha256:0b3596c50a5c700c9cb40ba8d86d9f2cc4807e9bedb06bcdf7fac85633e444dc", size = 10378, upload-time = "2026-04-13T23:21:45.386Z" }, +] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" }, + { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" }, + { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" }, + { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" }, + { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" }, + { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" }, + { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" }, + { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, + { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, + { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, + { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, + { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, + { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, + { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, + { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, + { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, + { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, + { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, + { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, + { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, + { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, + { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, +]