commit 31ec984482a49047e625d84d75ea828665c7c59b
parent be084ce91b81503c5bb3978eb760b34fdb050e9b
Author: andrewlaack-collab <andrew.laack@imbue.com>
Date: Mon, 2 Feb 2026 18:24:25 +0000
Refactoring for simple install (#6)
Diffstat:
180 files changed, 12676 insertions(+), 18622 deletions(-)
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
@@ -4,10 +4,20 @@ Vet is a library and CLI tool for verifying code quality and correctness.
## Installation
-From the repository root:
+**From git (pip/uv):**
```bash
-uv sync --project vet
+pip install git+https://github.com/imbue-ai/vet.git
+# or
+uv pip install git+https://github.com/imbue-ai/vet.git
+```
+
+**For development (from repository root):**
+
+```bash
+uv sync
+# or
+pip install -e .
```
## Usage
@@ -86,7 +96,7 @@ By default, `vet` runs all the registered issue identifiers and outputs all the
If you want to add a new issue identifier, you need to:
-1. Implement the `IssueIdentifier` protocol from `imbue_tools.repo_utils.data_types`.
+1. Implement the `IssueIdentifier` protocol from `vet.imbue_tools.repo_utils.data_types`.
2. Register the new issue identifier by adding it to `IDENTIFIERS` in `vet.issue_identifiers.registry`.
Based on your needs, instead of the above, you can also extend one of the existing batched zero-shot issue identifiers:
@@ -102,7 +112,7 @@ Refer to the source code for more details.
When creating a new entrypoint into vet, you must call `ensure_core_log_levels_configured()` to register the custom log levels used throughout the codebase.
```python
-from imbue_core.log_utils import ensure_core_log_levels_configured
+from vet.imbue_core.log_utils import ensure_core_log_levels_configured
ensure_core_log_levels_configured()
```
diff --git a/README.md b/README.md
@@ -10,6 +10,12 @@ It reviews git diffs, and optionally an agent's conversation history, to find is
pip install verify-everything
```
+Or install directly from git:
+
+```bash
+pip install git+https://github.com/imbue-ai/vet.git
+```
+
## Quickstart
Run Vet in the current repo:
diff --git a/imbue_core/README.md b/imbue_core/README.md
@@ -1,3 +0,0 @@
-# Imbue Core
-
-Utilities for `imbue-desktop`. This package is not meant to be installed on its own.
diff --git a/imbue_core/imbue_core/agents/agent_api/api.py b/imbue_core/imbue_core/agents/agent_api/api.py
@@ -1,59 +0,0 @@
-from __future__ import annotations
-
-from contextlib import contextmanager
-from functools import singledispatch
-from pathlib import Path
-from typing import Any
-from typing import ContextManager
-from typing import Iterator
-
-from imbue_core.agents.agent_api.claude.client import ClaudeCodeClient
-from imbue_core.agents.agent_api.claude.data_types import ClaudeCodeOptions
-from imbue_core.agents.agent_api.client import AgentClient
-from imbue_core.agents.agent_api.client import AgentOptionsT
-from imbue_core.agents.agent_api.client import CachedAgentClient
-from imbue_core.agents.agent_api.codex.client import CodexClient
-from imbue_core.agents.agent_api.codex.data_types import CodexOptions
-from imbue_core.agents.agent_api.data_types import AgentOptions
-
-
-@singledispatch
-def _build_client_from_options(
- options: AgentOptions,
-) -> ContextManager[AgentClient[Any]]:
- """Return a context manager that builds an AgentClient for the given options."""
- raise ValueError(f"Unsupported agent options type: {type(options).__name__}")
-
-
-@_build_client_from_options.register
-def _(options: ClaudeCodeOptions) -> ContextManager[AgentClient[ClaudeCodeOptions]]:
- return ClaudeCodeClient.build(options)
-
-
-@_build_client_from_options.register
-def _(options: CodexOptions) -> ContextManager[AgentClient[CodexOptions]]:
- return CodexClient.build(options)
-
-
-@contextmanager
-def get_agent_client(
- *,
- options: AgentOptionsT,
- cache_path: Path | None = None,
-) -> Iterator[AgentClient[AgentOptionsT]]:
- """Build and manage the lifecycle of an AgentClient based on the provided options.
-
- Args:
- options: AgentOptions instance describing which agent to run.
- cache_path: Optional path to use for caching agent interactions.
-
- Yields:
- An AgentClient (or CachedAgentClient) bound to the selected agent implementation.
- """
-
- with _build_client_from_options(options) as client:
- if cache_path is None:
- yield client
- return
-
- yield CachedAgentClient(client, cache_path)
diff --git a/imbue_core/imbue_core/agents/agent_api/cache_utils.py b/imbue_core/imbue_core/agents/agent_api/cache_utils.py
@@ -1,36 +0,0 @@
-import hashlib
-from pathlib import Path
-
-from imbue_core.agents.agent_api.data_types import AgentOptions
-from imbue_core.agents.agent_api.interaction import AgentInteractionRecord
-from imbue_core.caching import get_cache
-
-
-def _create_cache_key(prompt: str, options: AgentOptions) -> str:
- """Create a cache key for the given prompt and options."""
- return hashlib.md5(f"{prompt} | {options.model_dump_json() if options else ''}".encode()).hexdigest()
-
-
-def check_cache(cache_path: Path, prompt: str, options: AgentOptions) -> AgentInteractionRecord | None:
- """Check the cache for the given prompt and options."""
- cache_key = _create_cache_key(prompt, options)
- cache = get_cache(cache_path)
-
- with cache:
- value = cache.get(cache_key)
-
- if value is None:
- return None
- assert isinstance(value, str), f"Got value of type {type(value)} from cache, expected str"
- return AgentInteractionRecord.model_validate_json(value)
-
-
-def update_cache(
- agent_interaction: AgentInteractionRecord,
- cache_dir: Path,
-) -> None:
- """Save an agent interaction record to the cache."""
- cache = get_cache(cache_dir)
- cache_key = _create_cache_key(agent_interaction.prompt, agent_interaction.options)
- with cache:
- cache.set(cache_key, agent_interaction.model_dump_json())
diff --git a/imbue_core/imbue_core/agents/agent_api/claude/client.py b/imbue_core/imbue_core/agents/agent_api/claude/client.py
@@ -1,191 +0,0 @@
-import json
-import shutil
-import tempfile
-from contextlib import contextmanager
-from pathlib import Path
-from typing import Generator
-from typing import Iterator
-from typing import Self
-
-from loguru import logger
-
-from imbue_core.agents.agent_api.claude.data_types import ClaudeCodeOptions
-from imbue_core.agents.agent_api.claude.message_parser import parse_claude_message
-from imbue_core.agents.agent_api.client import RealAgentClient
-from imbue_core.agents.agent_api.data_types import AgentMessage
-from imbue_core.agents.agent_api.data_types import AgentResultMessage
-from imbue_core.agents.agent_api.errors import AgentCLINotFoundError
-from imbue_core.agents.agent_api.transport import AgentSubprocessCLITransport
-from imbue_core.agents.agent_api.transport import AgentSubprocessCLITransportOptions
-from imbue_core.agents.agent_api.transport import AgentTransport
-
-
-class ClaudeCodeClient(RealAgentClient[ClaudeCodeOptions]):
- """Claude Code client implementation.
-
- Most callers should obtain an instance through `get_agent_client(options=ClaudeCodeOptions(...))`,
- which takes care of building and tearing down the underlying CLI transport.
-
- Example:
- ```python
- with get_agent_client(options=ClaudeCodeOptions()) as client:
- for message in client.process_query(prompt="Hello"):
- print(message)
- ```
- """
-
- def __init__(self, options: ClaudeCodeOptions, transport: AgentTransport) -> None:
- super().__init__(options)
- self._transport = transport
-
- @classmethod
- @contextmanager
- def build(cls, options: ClaudeCodeOptions) -> Generator[Self, None, None]:
- cmd = cls._build_cli_cmd(options)
- with AgentSubprocessCLITransport.build(
- AgentSubprocessCLITransportOptions(
- cmd=cmd,
- cwd=options.cwd,
- extra_env_vars={"CLAUDE_CODE_ENTRYPOINT": "sdk-py"},
- )
- ) as transport:
- yield cls(options=options, transport=transport)
-
- def process_query(self, prompt: str) -> Iterator[AgentMessage]:
- logger.trace(
- "{client_name}: calling agent with prompt={prompt}",
- client_name=type(self).__name__,
- prompt=prompt,
- )
- # Claude code expects "User message" objects as inputs
- self._transport.send_request(
- [
- {
- "type": "user",
- "message": {
- "role": "user",
- "content": [{"type": "text", "text": prompt}],
- },
- }
- ],
- self._options,
- )
-
- for data in self._transport.receive_messages():
- logger.trace(
- "{client_name}: received raw JSON message={data}",
- client_name=type(self).__name__,
- data=data,
- )
-
- message = parse_claude_message(data)
- if message:
- yield message
-
- if isinstance(message, AgentResultMessage):
- break
-
- logger.trace(
- "{client_name}: finished calling agent with prompt={prompt}",
- client_name=type(self).__name__,
- prompt=prompt,
- )
-
- @staticmethod
- def _find_cli() -> str:
- """Find Claude Code CLI binary."""
- cli = shutil.which("claude")
- if cli:
- return cli
-
- locations = [
- # TODO: Document what these do. Does the path to claude inside the container need to be here?
- Path("/imbue_addons/bin/claude"),
- Path.home() / ".npm-global/bin/claude",
- Path("/usr/local/bin/claude"),
- Path.home() / ".local/bin/claude",
- Path.home() / "node_modules/.bin/claude",
- Path.home() / ".yarn/bin/claude",
- ]
-
- for path in locations:
- if path.exists() and path.is_file():
- return str(path)
-
- node_installed = shutil.which("node") is not None
-
- if not node_installed:
- raise AgentCLINotFoundError(
- "\n".join(
- [
- "Claude Code requires Node.js, which is not installed.",
- "Install Node.js from: https://nodejs.org/",
- "\nAfter installing Node.js, install Claude Code:",
- " npm install -g @anthropic-ai/claude-code",
- ]
- )
- )
-
- raise AgentCLINotFoundError(
- "\n".join(
- [
- "Claude Code not found. Install with:",
- " npm install -g @anthropic-ai/claude-code",
- "\nIf already installed locally, try:",
- ' export PATH="$HOME/node_modules/.bin:$PATH"',
- ]
- )
- )
-
- @classmethod
- def _build_cli_cmd(cls, options: ClaudeCodeOptions) -> list[str]:
- """Build CLI command with arguments."""
- if options.is_cached:
- # in this case, the cmd should never be used
- cmd = ["CACHED_CLAUDE_CODE_EXEC_PLACEHOLDER"]
- return cmd
- cli_path = str(options.cli_path) if options.cli_path is not None else cls._find_cli()
- cmd = [
- cli_path,
- "--output-format",
- "stream-json",
- "--input-format",
- "stream-json",
- "--verbose",
- ]
- cmd.extend(cls._build_cli_args(options))
- return cmd
-
- @staticmethod
- def _build_cli_args(options: ClaudeCodeOptions) -> list[str]:
- args = []
- if options.system_prompt:
- args.extend(["--system-prompt", options.system_prompt])
-
- if options.append_system_prompt:
- args.extend(["--append-system-prompt", options.append_system_prompt])
-
- if options.model:
- args.extend(["--model", options.model])
-
- if options.permission_prompt_tool_name:
- args.extend(["--permission-prompt-tool", options.permission_prompt_tool_name])
-
- if options.permission_mode:
- args.extend(["--permission-mode", options.permission_mode])
-
- if options.continue_conversation:
- args.append("--continue")
-
- if options.resume:
- args.extend(["--resume", options.resume])
-
- if options.mcp_servers:
- mcp_config_file = tempfile.NamedTemporaryFile(delete=False, suffix=".json")
- mcp_config_file.write(
- json.dumps({"mcpServers": {k: v.model_dump() for k, v in options.mcp_servers.items()}}).encode("utf-8")
- )
- args.extend(["--mcp-config", mcp_config_file.name])
-
- args.append("--print")
- return args
diff --git a/imbue_core/imbue_core/agents/agent_api/claude/data_types.py b/imbue_core/imbue_core/agents/agent_api/claude/data_types.py
@@ -1,79 +0,0 @@
-from pathlib import Path
-from typing import Literal
-
-from pydantic import Field
-
-from imbue_core.agents.agent_api.data_types import AgentOptions
-from imbue_core.agents.agent_api.data_types import AgentToolName
-from imbue_core.pydantic_serialization import SerializableModel
-
-ClaudePermissionMode = Literal["plan", "default", "acceptEdits", "bypassPermissions"]
-
-
-class ClaudeMcpStdioServerConfig(SerializableModel):
- """MCP stdio server configuration."""
-
- type: Literal["stdio"] = "stdio"
- command: str
- args: list[str] = Field(default_factory=list)
- env: dict[str, str] = Field(default_factory=dict)
-
-
-class ClaudeMcpHttpServerConfig(SerializableModel):
- """MCP HTTP server configuration."""
-
- type: Literal["http"] = "http"
- url: str
- headers: dict[str, str] | None = None
-
-
-ClaudeMcpServerConfig = ClaudeMcpStdioServerConfig | ClaudeMcpHttpServerConfig
-
-
-class ClaudeCodeOptions(AgentOptions):
- """Query options for Claude SDK."""
-
- object_type: Literal["ClaudeCodeOptions"] = "ClaudeCodeOptions"
-
- allowed_tools: list[str] = Field(default_factory=list)
- max_thinking_tokens: int = 8000
- system_prompt: str | None = None
- append_system_prompt: str | None = None
- mcp_tools: list[str] = Field(default_factory=list)
- mcp_servers: dict[str, ClaudeMcpServerConfig] = Field(default_factory=dict)
- permission_mode: ClaudePermissionMode | None = None
- continue_conversation: bool = False
- resume: str | None = None
- max_turns: int | None = None
- disallowed_tools: list[str] = Field(default_factory=list)
- model: str | None = None
- permission_prompt_tool_name: str | None = None
- # Optional override for the Claude CLI path
- cli_path: Path | None = None
- is_cached: bool = False
-
-
-CLAUDE_TOOLS = (
- AgentToolName.READ,
- AgentToolName.WRITE,
- AgentToolName.EDIT,
- AgentToolName.MULTI_EDIT,
- AgentToolName.GLOB,
- AgentToolName.NOTEBOOK_READ,
- AgentToolName.NOTEBOOK_EDIT,
- AgentToolName.LS,
- AgentToolName.GREP,
- AgentToolName.BASH,
- AgentToolName.BASH_OUTPUT,
- AgentToolName.KILL_SHELL,
- AgentToolName.WEB_SEARCH,
- AgentToolName.WEB_FETCH,
- AgentToolName.TASK,
- AgentToolName.TODO_READ,
- AgentToolName.TODO_WRITE,
- AgentToolName.SLASH_COMMAND,
- AgentToolName.EXIT_PLAN_MODE,
- AgentToolName.MCP_TOOL,
- AgentToolName.LIST_MCP_RESOURCES,
- AgentToolName.READ_MCP_RESOURCE,
-)
diff --git a/imbue_core/imbue_core/agents/agent_api/claude/message_parser.py b/imbue_core/imbue_core/agents/agent_api/claude/message_parser.py
@@ -1,117 +0,0 @@
-from typing import Any
-from typing import assert_never
-
-from imbue_core.agents.agent_api.data_types import AgentAssistantMessage
-from imbue_core.agents.agent_api.data_types import AgentContentBlock
-from imbue_core.agents.agent_api.data_types import AgentMessage
-from imbue_core.agents.agent_api.data_types import AgentResultMessage
-from imbue_core.agents.agent_api.data_types import AgentSystemEventType
-from imbue_core.agents.agent_api.data_types import AgentSystemMessage
-from imbue_core.agents.agent_api.data_types import AgentTextBlock
-from imbue_core.agents.agent_api.data_types import AgentThinkingBlock
-from imbue_core.agents.agent_api.data_types import AgentToolResultBlock
-from imbue_core.agents.agent_api.data_types import AgentToolUseBlock
-from imbue_core.agents.agent_api.data_types import AgentUsage
-from imbue_core.agents.agent_api.data_types import AgentUserMessage
-
-
-def parse_claude_message(data: dict[str, Any]) -> AgentMessage | None:
- """Parse message from CLI output using unified types.
-
- Reference:
- https://github.com/anthropics/claude-agent-sdk-python/blob/main/src/claude_agent_sdk/_internal/message_parser.py
- https://docs.claude.com/en/api/agent-sdk/typescript#sdkmessage
- https://docs.claude.com/en/api/agent-sdk/python#message-types
- """
-
- match data["type"]:
- case "user":
- return AgentUserMessage(content=parse_claude_content_blocks(data), original_message=data)
-
- case "assistant":
- return AgentAssistantMessage(content=parse_claude_content_blocks(data), original_message=data)
-
- case "system":
- # Normalize system event types
- event_type = parse_claude_system_event_type(data.get("subtype", ""))
- return AgentSystemMessage(
- event_type=event_type,
- session_id=data.get("session_id"),
- error=data.get("error"),
- original_message=data,
- )
-
- case "result":
- # Build normalized usage
- usage = None
- raw_usage = data.get("usage")
- if raw_usage or data.get("total_cost_usd"):
- usage = AgentUsage(
- input_tokens=raw_usage.get("input_tokens") if raw_usage else None,
- output_tokens=raw_usage.get("output_tokens") if raw_usage else None,
- cached_tokens=(raw_usage.get("cache_read_input_tokens") if raw_usage else None),
- total_tokens=(
- raw_usage.get("input_tokens", 0) + raw_usage.get("output_tokens", 0) if raw_usage else None
- ),
- total_cost_usd=data.get("total_cost_usd"),
- )
-
- return AgentResultMessage(
- session_id=data["session_id"],
- is_error=data["is_error"],
- duration_ms=data.get("duration_ms"),
- api_duration_ms=data.get("duration_api_ms"),
- num_turns=data.get("num_turns"),
- usage=usage,
- result=data.get("result"),
- error=data.get("error") if data["is_error"] else None,
- original_message=data,
- )
-
- case _ as unreachable:
- assert_never(unreachable)
-
-
-def parse_claude_system_event_type(subtype: str) -> AgentSystemEventType:
- """Parse Claude system event subtype to unified event type."""
- subtype_lower = subtype.lower()
-
- # TODO add other system event types as we find them
- # basically the documentattion doesn't mention any other system event types
- # other than init AFAIKT
- if "init" in subtype_lower:
- return AgentSystemEventType.SESSION_STARTED
- else:
- return AgentSystemEventType.OTHER
-
-
-def parse_claude_content_blocks(data: dict[str, Any]) -> list[AgentContentBlock]:
- return [parse_claude_content_block(block) for block in data["message"]["content"]]
-
-
-def parse_claude_content_block(block: dict[str, Any]) -> AgentContentBlock:
- """Parse content block from CLI output using unified types."""
-
- match block["type"]:
- case "text":
- return AgentTextBlock(text=block["text"])
-
- case "thinking":
- # Claude Code thinking blocks
- return AgentThinkingBlock(
- content=block.get("thinking", ""),
- thinking_tokens=block.get("thinking_tokens"),
- )
-
- case "tool_use":
- return AgentToolUseBlock(id=block["id"], name=block["name"], input=block["input"])
-
- case "tool_result":
- return AgentToolResultBlock(
- tool_use_id=block["tool_use_id"],
- content=block.get("content"),
- is_error=block.get("is_error"),
- )
-
- case _ as unreachable:
- assert_never(unreachable)
diff --git a/imbue_core/imbue_core/agents/agent_api/client.py b/imbue_core/imbue_core/agents/agent_api/client.py
@@ -1,89 +0,0 @@
-import abc
-from pathlib import Path
-from typing import Generic
-from typing import Iterator
-from typing import TypeVar
-
-from imbue_core.agents.agent_api.cache_utils import check_cache
-from imbue_core.agents.agent_api.cache_utils import update_cache
-from imbue_core.agents.agent_api.data_types import AgentMessage
-from imbue_core.agents.agent_api.data_types import AgentOptions
-from imbue_core.agents.agent_api.interaction import AgentInteraction
-from imbue_core.agents.agent_api.interaction import AgentInteractionRecord
-
-AgentOptionsT = TypeVar("AgentOptionsT", bound=AgentOptions)
-
-
-class AgentClient(abc.ABC, Generic[AgentOptionsT]):
- """Base code agent client interface.
-
- This client defines the interface for launching and interacting with a coding agent (e.g., ClaudeCode, Codex, etc.)
-
- Clients are usually created through `get_agent_client`, which selects the right concrete implementation
- and manages any transports. Direct subclasses only need to implement `process_query`.
- """
-
- def __init__(self, options: AgentOptionsT) -> None:
- self._options = options
-
- @abc.abstractmethod
- def process_query(self, prompt: str) -> Iterator[AgentMessage]:
- """Call the underlying agent to process a query."""
-
-
-class RealAgentClient(AgentClient[AgentOptionsT]):
- """Agent client that is not cached or dummy; it runs real commands."""
-
- @staticmethod
- @abc.abstractmethod
- def _find_cli() -> str:
- """Find the CLI binary for the agent."""
-
- @classmethod
- @abc.abstractmethod
- def _build_cli_cmd(cls, options: AgentOptionsT) -> list[str]:
- """Build the CLI command for the agent."""
-
- @staticmethod
- @abc.abstractmethod
- def _build_cli_args(options: AgentOptionsT) -> list[str]:
- """Build the CLI arguments for the agent."""
-
-
-class CachedAgentClient(AgentClient[AgentOptionsT]):
- """Cached agent client implementation.
-
- This client is a wrapper around an agent client that caches the agent responses.
- """
-
- def __init__(self, client: AgentClient[AgentOptionsT], cache_path: Path) -> None:
- super().__init__(client._options)
- self._client = client
- self._cache_path = cache_path
-
- def process_query(self, prompt: str) -> Iterator[AgentMessage]:
- cache_path = self._cache_path
- if cache_path is not None:
- cache_record = check_cache(cache_path, prompt, self._client._options)
- if cache_record is not None:
- for message in cache_record.messages:
- yield message
- return
-
- agent_interaction = AgentInteraction(prompt, self._client._options)
- for message in self._client.process_query(prompt):
- agent_interaction.put(message)
- yield message
-
- # NOTE we only cache full interactions given the 'process_query' method is called till
- # the generator is exhausted.
- # This means that if the generator is not exhausted, the cache will not be updated.
- # If we do want a way to still cache interactions, even if we early exit the generator,
- # then we could use a separate thread to get the agent response and cache it in the background.
- agent_interaction_record = AgentInteractionRecord.from_agent_interaction(agent_interaction)
- update_cache(agent_interaction_record, cache_path)
-
- @property
- def client(self) -> AgentClient[AgentOptionsT]:
- """Get the underlying client."""
- return self._client
diff --git a/imbue_core/imbue_core/agents/agent_api/codex/client.py b/imbue_core/imbue_core/agents/agent_api/codex/client.py
@@ -1,166 +0,0 @@
-import json
-import shutil
-from contextlib import contextmanager
-from pathlib import Path
-from typing import Generator
-from typing import Iterator
-from typing import Self
-
-from loguru import logger
-
-from imbue_core.agents.agent_api.client import RealAgentClient
-from imbue_core.agents.agent_api.codex.data_types import CodexOptions
-from imbue_core.agents.agent_api.codex.message_parser import parse_codex_event
-from imbue_core.agents.agent_api.data_types import AgentMessage
-from imbue_core.agents.agent_api.data_types import AgentSystemMessage
-from imbue_core.agents.agent_api.errors import AgentCLINotFoundError
-from imbue_core.agents.agent_api.transport import AgentSubprocessCLITransport
-from imbue_core.agents.agent_api.transport import AgentSubprocessCLITransportOptions
-
-
-class CodexClient(RealAgentClient[CodexOptions]):
- """Codex CLI client implementation."""
-
- def __init__(self, options: CodexOptions) -> None:
- super().__init__(options=options)
- self._session_id: str | None = options.resume_session_id
-
- @classmethod
- @contextmanager
- def build(cls, options: CodexOptions) -> Generator[Self, None, None]:
- yield cls(options=options)
-
- def process_query(self, prompt: str) -> Iterator[AgentMessage]:
- logger.trace(
- "{client_name}: calling agent with prompt={prompt}",
- client_name=type(self).__name__,
- prompt=prompt,
- )
-
- # NOTE: (2025-11-20) Codex CLI does not support streaming inputs, and only supports using codex CLI via
- # non-interactive mode, where each call is a new process.
- # So here we just create a new transport for each call, and handle things like resuming the session as
- # needed.
- options = self._options
- if self._session_id is not None and self._session_id != self._options.resume_session_id:
- # Inject the current session id into the options before building the command
- options = self._options.model_copy(update={"resume_session_id": self._session_id})
- cmd = self._build_cli_cmd(options)
- with AgentSubprocessCLITransport.build(
- AgentSubprocessCLITransportOptions(cmd=[*cmd, prompt], cwd=options.cwd)
- ) as transport:
- transport.send_request([prompt], options)
-
- thread_id: str | None = None
- for data in transport.receive_messages():
- logger.trace(
- "{client_name}: received raw JSON message={data}",
- client_name=type(self).__name__,
- data=data,
- )
-
- message = parse_codex_event(data, thread_id)
- if message:
- if isinstance(message, AgentSystemMessage):
- thread_id = message.session_id
- # Store the new session id for subsequent calls to process_query on this client
- self._session_id = message.session_id
-
- yield message
-
- logger.trace(
- "{client_name}: finished calling agent with prompt={prompt}",
- client_name=type(self).__name__,
- prompt=prompt,
- )
-
- @staticmethod
- def _find_cli() -> str:
- """Find Codex CLI binary."""
- cli = shutil.which("codex")
- if cli:
- return cli
-
- locations = [
- Path("/usr/local/bin/codex"),
- Path.home() / ".local/bin/codex",
- Path.home() / "node_modules/.bin/codex",
- Path.home() / ".npm-global/bin/codex",
- ]
-
- for path in locations:
- if path.exists() and path.is_file():
- return str(path)
-
- node_installed = shutil.which("node") is not None
- npm_installed = shutil.which("npm") is not None
-
- if not node_installed or not npm_installed:
- raise AgentCLINotFoundError(
- "\n".join(
- [
- "Codex CLI requires Node.js and npm, which may not be installed.",
- "Install Node.js from: https://nodejs.org/",
- "\nAfter installing Node.js, install Codex CLI:",
- " npm install -g @openai/codex",
- ]
- )
- )
-
- raise AgentCLINotFoundError(
- "\n".join(
- [
- "Codex CLI not found. Install with:",
- " npm install -g @openai/codex",
- "\nOr via Homebrew:",
- " brew install codex",
- "\nIf already installed locally, try:",
- ' export PATH="$HOME/node_modules/.bin:$PATH"',
- ]
- )
- )
-
- @classmethod
- def _build_cli_cmd(cls, options: CodexOptions) -> list[str]:
- """Build CLI command with arguments."""
- if options.is_cached:
- # in this case, the cmd should never be used
- cmd = ["CACHED_CODEX_EXEC_PLACEHOLDER"]
- return cmd
- cli_path = str(options.cli_path) if options.cli_path is not None else cls._find_cli()
- cmd = [cli_path, "exec"]
- cmd.extend(cls._build_cli_args(options))
- return cmd
-
- @staticmethod
- def _build_cli_args(options: CodexOptions) -> list[str]:
- args = []
- # Permissions flags
- if options.approval_mode:
- args.extend(["-c", f"'approval_mode={options.approval_mode}'"])
- if options.sandbox_mode:
- args.extend(["--sandbox", options.sandbox_mode])
- if options.approval_policy:
- args.extend(["-c", f"'approval={options.approval_policy}'"])
-
- # JSON streaming output
- args.append("--json")
-
- # Model selection
- if options.model:
- args.extend(["--model", options.model])
-
- # Skip git repo check
- if options.skip_git_repo_check:
- args.append("--skip-git-repo-check")
-
- # Output schema for structured output
- if options.output_schema:
- args.extend(["--output-schema", json.dumps(options.output_schema)])
-
- # Session resumption
- if options.resume_last:
- args.extend(["resume", "--last"])
- elif options.resume_session_id:
- args.extend(["resume", options.resume_session_id])
- return args
diff --git a/imbue_core/imbue_core/agents/agent_api/codex/data_types.py b/imbue_core/imbue_core/agents/agent_api/codex/data_types.py
@@ -1,241 +0,0 @@
-"""Data types for Codex agent integration."""
-
-from pathlib import Path
-from typing import Annotated
-from typing import Any
-from typing import Literal
-
-from pydantic import Field
-from pydantic import Tag
-
-from imbue_core.agents.agent_api.data_types import AgentOptions
-from imbue_core.agents.agent_api.data_types import AgentToolName
-from imbue_core.pydantic_serialization import SerializableModel
-from imbue_core.pydantic_serialization import build_discriminator
-
-# https://developers.openai.com/codex/cli/features#approval-modes
-CodexApprovalMode = Literal["auto", "read-only", "full-access"] | None
-
-# https://developers.openai.com/codex/cli/reference, --sandbox options
-CodexSandboxMode = Literal["read-only", "workspace-write", "danger-full-access"] | None
-
-# https://developers.openai.com/codex/cli/reference, --ask-for-approval options
-CodexApprovalPolicy = Literal["untrusted", "on-failure", "on-request", "never"] | None
-
-
-class CodexOptions(AgentOptions):
- """Options for Codex CLI execution."""
-
- object_type: Literal["CodexOptions"] = "CodexOptions"
-
- approval_mode: CodexApprovalMode = None
- sandbox_mode: CodexSandboxMode = None
- approval_policy: CodexApprovalPolicy = None
- model: str | None = None
- system_prompt: str | None = None
- image_paths: list[Path] = Field(default_factory=list)
- skip_git_repo_check: bool = False
- output_schema: dict[str, Any] | None = None
- # Session management
- resume_session_id: str | None = None
- resume_last: bool = False
- thread_id: str | None = None
- # Optional override for the Codex CLI path
- cli_path: Path | None = None
- is_cached: bool = False
-
-
-# Codex item types
-# Ref: https://github.com/openai/codex/blob/main/sdk/typescript/src/items.ts
-# Ref: https://github.com/openai/codex/blob/main/codex-rs/exec/src/exec_events.rs
-
-
-# The status of a command execution.
-CommandExecutionStatus = Literal["in_progress", "completed", "failed"]
-
-
-class CodexCommandExecutionItem(SerializableModel):
- type: Literal["command_execution"] = "command_execution"
- id: str
- command: str
- aggregated_output: str
- exit_code: int | None = None
- status: CommandExecutionStatus
-
-
-# Indicates the type of the file change.
-PatchChangeKind = Literal["add", "delete", "update"]
-
-
-class CodexFileUpdateChange(SerializableModel):
- path: str
- kind: PatchChangeKind
-
-
-# The status of a file change.
-PatchApplyStatus = Literal["completed", "failed"]
-
-
-class CodexFileChangeItem(SerializableModel):
- type: Literal["file_change"] = "file_change"
- id: str
- changes: list[CodexFileUpdateChange]
- status: PatchApplyStatus
-
-
-# The status of an MCP tool call.
-McpToolCallStatus = Literal["in_progress", "completed", "failed"]
-
-
-class CodexMcpToolCallItem(SerializableModel):
- type: Literal["mcp_tool_call"] = "mcp_tool_call"
- id: str
- server: str
- tool: str
- status: McpToolCallStatus
-
-
-class CodexAgentMessageItem(SerializableModel):
- type: Literal["agent_message"] = "agent_message"
- id: str
- text: str
-
-
-class CodexReasoningItem(SerializableModel):
- type: Literal["reasoning"] = "reasoning"
- id: str
- text: str
-
-
-class CodexWebSearchItem(SerializableModel):
- type: Literal["web_search"] = "web_search"
- id: str
- query: str
-
-
-class CodexErrorItem(SerializableModel):
- type: Literal["error"] = "error"
- id: str
- message: str
-
-
-class CodexTodoItem(SerializableModel):
- text: str
- completed: bool
-
-
-class CodexTodoListItem(SerializableModel):
- type: Literal["todo_list"] = "todo_list"
- id: str
- items: list[CodexTodoItem]
-
-
-# Canonical union of thread items and their type-specific payloads.
-CodexThreadItemUnion = Annotated[
- (
- Annotated[CodexAgentMessageItem, Tag("agent_message")]
- | Annotated[CodexReasoningItem, Tag("reasoning")]
- | Annotated[CodexCommandExecutionItem, Tag("command_execution")]
- | Annotated[CodexFileChangeItem, Tag("file_change")]
- | Annotated[CodexMcpToolCallItem, Tag("mcp_tool_call")]
- | Annotated[CodexWebSearchItem, Tag("web_search")]
- | Annotated[CodexTodoListItem, Tag("todo_list")]
- | Annotated[CodexErrorItem, Tag("error")]
- ),
- build_discriminator("type"),
-]
-
-
-# Codex (JSONL) event stream models
-# Ref:https://github.com/openai/codex/blob/main/sdk/typescript/src/events.ts
-
-
-class CodexThreadStartedEvent(SerializableModel):
- type: Literal["thread.started"] = "thread.started"
- thread_id: str
-
-
-class CodexTurnStartedEvent(SerializableModel):
- type: Literal["turn.started"] = "turn.started"
-
-
-class CodexUsage(SerializableModel):
- input_tokens: int
- cached_input_tokens: int
- output_tokens: int
-
-
-class CodexTurnCompletedEvent(SerializableModel):
- type: Literal["turn.completed"] = "turn.completed"
- usage: CodexUsage
-
-
-class CodexThreadError(SerializableModel):
- message: str
-
-
-class CodexTurnFailedEvent(SerializableModel):
- type: Literal["turn.failed"] = "turn.failed"
- error: CodexThreadError
-
-
-class CodexItemStartedEvent(SerializableModel):
- type: Literal["item.started"] = "item.started"
- item: CodexThreadItemUnion
-
-
-class CodexItemUpdatedEvent(SerializableModel):
- type: Literal["item.updated"] = "item.updated"
- item: CodexThreadItemUnion
-
-
-class CodexItemCompletedEvent(SerializableModel):
- type: Literal["item.completed"] = "item.completed"
- item: CodexThreadItemUnion
-
-
-class CodexThreadErrorEvent(SerializableModel):
- type: Literal["error"] = "error"
- message: str
-
-
-CodexThreadEvent = Annotated[
- (
- Annotated[CodexThreadStartedEvent, Tag("thread.started")]
- | Annotated[CodexTurnStartedEvent, Tag("turn.started")]
- | Annotated[CodexTurnCompletedEvent, Tag("turn.completed")]
- | Annotated[CodexTurnFailedEvent, Tag("turn.failed")]
- | Annotated[CodexItemStartedEvent, Tag("item.started")]
- | Annotated[CodexItemUpdatedEvent, Tag("item.updated")]
- | Annotated[CodexItemCompletedEvent, Tag("item.completed")]
- | Annotated[CodexThreadErrorEvent, Tag("error")]
- ),
- build_discriminator("type"),
-]
-
-# TODO: some of these might not actually be valid for codex!
-CODEX_TOOLS = (
- AgentToolName.AGENT,
- AgentToolName.BASH,
- AgentToolName.EDIT,
- AgentToolName.GLOB,
- AgentToolName.GREP,
- AgentToolName.LS,
- AgentToolName.MULTI_EDIT,
- AgentToolName.NOTEBOOK_EDIT,
- AgentToolName.NOTEBOOK_READ,
- AgentToolName.READ,
- AgentToolName.TODO_READ,
- AgentToolName.TODO_WRITE,
- AgentToolName.WEB_FETCH,
- AgentToolName.WEB_SEARCH,
- AgentToolName.WRITE,
- AgentToolName.COMPUTER,
- AgentToolName.MEMORY,
- AgentToolName.OTHER,
- AgentToolName.CODE_EXECUTION,
- AgentToolName.BASH_CODE_EXECUTION,
- AgentToolName.TEXT_EDITOR_CODE_EXECUTION,
- AgentToolName.COMMAND_EXECUTION,
- AgentToolName.FILE_CHANGE,
-)
diff --git a/imbue_core/imbue_core/agents/agent_api/codex/message_parser.py b/imbue_core/imbue_core/agents/agent_api/codex/message_parser.py
@@ -1,218 +0,0 @@
-from typing import Any
-from typing import assert_never
-
-from pydantic import TypeAdapter
-
-from imbue_core.agents.agent_api.codex.data_types import CodexAgentMessageItem
-from imbue_core.agents.agent_api.codex.data_types import CodexCommandExecutionItem
-from imbue_core.agents.agent_api.codex.data_types import CodexErrorItem
-from imbue_core.agents.agent_api.codex.data_types import CodexFileChangeItem
-from imbue_core.agents.agent_api.codex.data_types import CodexItemCompletedEvent
-from imbue_core.agents.agent_api.codex.data_types import CodexItemStartedEvent
-from imbue_core.agents.agent_api.codex.data_types import CodexItemUpdatedEvent
-from imbue_core.agents.agent_api.codex.data_types import CodexMcpToolCallItem
-from imbue_core.agents.agent_api.codex.data_types import CodexReasoningItem
-from imbue_core.agents.agent_api.codex.data_types import CodexThreadErrorEvent
-from imbue_core.agents.agent_api.codex.data_types import CodexThreadEvent
-from imbue_core.agents.agent_api.codex.data_types import CodexThreadItemUnion
-from imbue_core.agents.agent_api.codex.data_types import CodexThreadStartedEvent
-from imbue_core.agents.agent_api.codex.data_types import CodexTodoListItem
-from imbue_core.agents.agent_api.codex.data_types import CodexTurnCompletedEvent
-from imbue_core.agents.agent_api.codex.data_types import CodexTurnFailedEvent
-from imbue_core.agents.agent_api.codex.data_types import CodexTurnStartedEvent
-from imbue_core.agents.agent_api.codex.data_types import CodexWebSearchItem
-from imbue_core.agents.agent_api.data_types import AgentAssistantMessage
-from imbue_core.agents.agent_api.data_types import AgentContentBlock
-from imbue_core.agents.agent_api.data_types import AgentMessage
-from imbue_core.agents.agent_api.data_types import AgentResultMessage
-from imbue_core.agents.agent_api.data_types import AgentSystemEventType
-from imbue_core.agents.agent_api.data_types import AgentSystemMessage
-from imbue_core.agents.agent_api.data_types import AgentTextBlock
-from imbue_core.agents.agent_api.data_types import AgentThinkingBlock
-from imbue_core.agents.agent_api.data_types import AgentToolResultBlock
-from imbue_core.agents.agent_api.data_types import AgentToolUseBlock
-from imbue_core.agents.agent_api.data_types import AgentUsage
-
-
-def parse_codex_event(data: dict[str, Any], thread_id: str | None = None) -> AgentMessage | None:
- """Parse Codex event into unified message.
-
- Reference:
- https://github.com/openai/codex/blob/main/docs/exec.md
- https://github.com/openai/codex/blob/main/sdk/typescript/src/events.ts
- """
- codex_event = TypeAdapter(CodexThreadEvent).validate_python(data)
- match codex_event:
- case CodexThreadStartedEvent():
- return AgentSystemMessage(
- event_type=AgentSystemEventType.SESSION_STARTED,
- session_id=codex_event.thread_id,
- original_message=data,
- )
-
- case CodexTurnStartedEvent():
- # Turn started within a thread. Nothing to do
- return None
-
- case CodexTurnCompletedEvent():
- assert thread_id is not None, "thread_id is required for turn.completed event"
- usage = AgentUsage(
- input_tokens=codex_event.usage.input_tokens,
- output_tokens=codex_event.usage.output_tokens,
- cached_tokens=codex_event.usage.cached_input_tokens,
- total_tokens=codex_event.usage.input_tokens + codex_event.usage.output_tokens,
- )
- return AgentResultMessage(
- session_id=thread_id,
- is_error=False,
- usage=usage,
- original_message=data,
- )
-
- case CodexTurnFailedEvent():
- assert thread_id is not None, "thread_id is required for turn.failed event"
- return AgentResultMessage(
- session_id=thread_id,
- is_error=True,
- error=codex_event.error.message,
- usage=None,
- original_message=data,
- )
-
- case CodexItemStartedEvent():
- content_blocks = parse_codex_item(codex_event.item)
- return AgentAssistantMessage(content=content_blocks, original_message=data)
-
- case CodexItemUpdatedEvent():
- # Intermediate item, don't return anything
- return None
-
- case CodexItemCompletedEvent():
- content_blocks = parse_codex_item(codex_event.item)
- return AgentAssistantMessage(content=content_blocks, original_message=data)
-
- case CodexThreadErrorEvent():
- return AgentResultMessage(
- session_id=thread_id or "",
- is_error=True,
- error=codex_event.message,
- usage=None,
- original_message=data,
- )
- case _ as unreachable:
- assert_never(unreachable)
-
-
-def parse_codex_item(
- item_data: dict[str, Any] | CodexThreadItemUnion,
-) -> list[AgentContentBlock]:
- """Parse Codex item into unified content blocks.
-
- Refs:
- https://github.com/openai/codex/blob/main/sdk/typescript/src/items.ts
- """
- if isinstance(item_data, dict):
- codex_item = TypeAdapter(CodexThreadItemUnion).validate_python(item_data)
- else:
- codex_item = item_data
-
- match codex_item:
- case CodexAgentMessageItem():
- return [AgentTextBlock(text=codex_item.text)]
-
- case CodexReasoningItem():
- return [AgentThinkingBlock(content=codex_item.text)]
-
- case CodexErrorItem():
- return [AgentTextBlock(text=f"[Error: {codex_item.message}]")]
-
- case CodexCommandExecutionItem():
- if codex_item.status == "in_progress":
- return [
- AgentToolUseBlock(
- id=codex_item.id,
- name=codex_item.type,
- input={"command": codex_item.command},
- )
- ]
- return [
- AgentToolResultBlock(
- tool_use_id=codex_item.id,
- content=codex_item.aggregated_output,
- exit_code=codex_item.exit_code,
- is_error=codex_item.exit_code != 0,
- )
- ]
-
- case CodexFileChangeItem():
- is_error = codex_item.status == "failed"
- return [
- AgentToolUseBlock(
- id=codex_item.id,
- name=codex_item.type,
- input={"changes": [change.model_dump() for change in codex_item.changes]},
- ),
- AgentToolResultBlock(
- tool_use_id=codex_item.id,
- content=[change.model_dump() for change in codex_item.changes],
- is_error=is_error,
- exit_code=-1 if is_error else 0,
- ),
- ]
-
- case CodexMcpToolCallItem():
- if codex_item.status == "in_progress":
- return [
- AgentToolUseBlock(
- id=codex_item.id,
- name=codex_item.type,
- input={"server": codex_item.server, "tool": codex_item.tool},
- )
- ]
- # NOTE: currently (24-oct-2025) the MCP tool call item is not really well defined
- # it does not have a result field or anything. So for now, we just return the server and tool as the content.
- return [
- AgentToolResultBlock(
- tool_use_id=codex_item.id,
- content=[{"server": codex_item.server, "tool": codex_item.tool}],
- is_error=codex_item.status == "failed",
- exit_code=-1 if codex_item.status == "failed" else 0,
- )
- ]
-
- case CodexWebSearchItem():
- # NOTE: currently (24-oct-2025) the web search item is not really well defined
- # i.e. it only has a query field, and no other fields like results, progress, etc.
- # so for now, so that each tool use has a matching result, we just return the query as the content.
- return [
- AgentToolUseBlock(
- id=codex_item.id,
- name=codex_item.type,
- input={"query": codex_item.query},
- ),
- AgentToolResultBlock(
- tool_use_id=codex_item.id,
- content=codex_item.query,
- # No error reported for web search
- is_error=False,
- exit_code=0,
- ),
- ]
-
- case CodexTodoListItem():
- return [
- AgentToolUseBlock(
- id=codex_item.id,
- name=codex_item.type,
- input={"todos": [item.model_dump() for item in codex_item.items]},
- ),
- AgentToolResultBlock(
- tool_use_id=codex_item.id,
- content=[item.model_dump() for item in codex_item.items],
- is_error=False,
- exit_code=0,
- ),
- ]
-
- case _ as unreachable:
- assert_never(unreachable)
diff --git a/imbue_core/imbue_core/agents/agent_api/data_types.py b/imbue_core/imbue_core/agents/agent_api/data_types.py
@@ -1,252 +0,0 @@
-import enum
-from pathlib import Path
-from typing import Annotated
-from typing import Any
-from typing import Literal
-
-from pydantic import Field
-from pydantic import Tag
-
-from imbue_core.pydantic_serialization import SerializableModel
-from imbue_core.pydantic_serialization import build_discriminator
-
-AgentPermissionMode = Literal["default", "acceptEdits", "bypassPermissions"]
-
-
-class AgentToolName(enum.StrEnum):
- """Enumeration of all known coding agent tools across Claude Code and Codex.
-
- This is a superset of tools available across different coding agents.
- Not all tools are available in all agents.
- """
-
- # File operations
- READ = "Read"
- WRITE = "Write"
- EDIT = "Edit"
- MULTI_EDIT = "MultiEdit"
- GLOB = "Glob"
- NOTEBOOK_READ = "NotebookRead"
- NOTEBOOK_EDIT = "NotebookEdit"
- LS = "LS"
-
- # Search operations
- GREP = "Grep"
-
- # Execution tools
- BASH = "Bash"
- BASH_OUTPUT = "BashOutput"
- KILL_SHELL = "KillShell"
-
- # Web operations
- WEB_SEARCH = "WebSearch"
- WEB_FETCH = "WebFetch"
-
- # Agent orchestration
- TASK = "Task"
- TODO_READ = "TodoRead"
- TODO_WRITE = "TodoWrite"
- SLASH_COMMAND = "SlashCommand"
- EXIT_PLAN_MODE = "exit_plan_mode"
-
- # MCP tools
- MCP_TOOL = "mcp_tool" # Generic MCP tool prefix
- LIST_MCP_RESOURCES = "ListMcpResourcesTool"
- READ_MCP_RESOURCE = "ReadMcpResourceTool"
-
- # Code execution
- CODE_EXECUTION = "code_execution"
- BASH_CODE_EXECUTION = "bash_code_execution"
- TEXT_EDITOR_CODE_EXECUTION = "text_editor_code_execution"
-
- # Codex-specific operations
- COMMAND_EXECUTION = "command_execution" # Codex's command execution
- FILE_CHANGE = "file_change" # Codex's file change operation
-
- # Other tools
- AGENT = "Agent"
- COMPUTER = "computer" # Computer use capability
- MEMORY = "memory" # Memory storage
- OTHER = "other" # Catch-all for unknown/custom tools
-
-
-# TODO: these are not, in the strict sense, read-only; perhaps we should have finer gradations
-READ_ONLY_TOOLS = (
- AgentToolName.TASK,
- AgentToolName.READ,
- AgentToolName.GLOB,
- AgentToolName.GREP,
- AgentToolName.LS,
- AgentToolName.BASH,
- AgentToolName.NOTEBOOK_READ,
- AgentToolName.TODO_READ,
- AgentToolName.TODO_WRITE,
- AgentToolName.WEB_FETCH,
- AgentToolName.WEB_SEARCH,
-)
-
-
-# Content block types
-class AgentTextBlock(SerializableModel):
- """Text content block.
-
- Represents plain text output from the agent.
- """
-
- text: str
-
-
-class AgentThinkingBlock(SerializableModel):
- """Agent's internal reasoning/thinking block.
-
- Represents the agent's thought process or reasoning, which may be hidden
- from the end user in some interfaces.
- """
-
- content: str
- thinking_tokens: int | None = Field(default=None, description="Number of tokens used for thinking")
-
-
-class AgentToolUseBlock(SerializableModel):
- """Tool invocation request.
-
- Represents a request from the agent to use a specific tool.
- """
-
- id: str
- name: AgentToolName | str # allow str for flexibility
- input: dict[str, Any]
-
-
-class AgentToolResultBlock(SerializableModel):
- """Tool execution result.
-
- Represents the result of executing a tool, which is fed back to the agent.
- """
-
- tool_use_id: str
- content: str | list[dict[str, Any]] | None = None
- is_error: bool | None = None
- exit_code: int | None = Field(default=None, description="Exit code for command executions")
-
-
-AgentContentBlock = AgentTextBlock | AgentThinkingBlock | AgentToolUseBlock | AgentToolResultBlock
-
-
-class AgentSystemEventType(enum.StrEnum):
- """System event types
-
- Super set of system event types across all agents.
- """
-
- SESSION_STARTED = "session_started"
- SESSION_RESUMED = "session_resumed"
- TURN_STARTED = "turn_started"
- TURN_COMPLETED = "turn_completed"
- TURN_FAILED = "turn_failed"
- # For agent-specific events that don't fit into the above categories
- OTHER = "other"
-
-
-# Message types (`type` field is required for serialization)
-class AgentUserMessage(SerializableModel):
- """User message.
-
- Represents input from the user to the agent.
- """
-
- object_type: Literal["AgentUserMessage"] = "AgentUserMessage"
- content: str | list[AgentContentBlock]
- original_message: dict[str, Any] | None = Field(default=None, description="Original agent-specific message data")
-
-
-class AgentAssistantMessage(SerializableModel):
- """Assistant message with content blocks.
-
- Represents output from the agent, which may include text, thinking,
- tool uses, and tool results.
- """
-
- object_type: Literal["AgentAssistantMessage"] = "AgentAssistantMessage"
- content: list[AgentContentBlock]
- original_message: dict[str, Any] | None = Field(default=None, description="Original agent-specific message data")
-
-
-class AgentSystemMessage(SerializableModel):
- """System message with normalized event data.
-
- Represents lifecycle events from the agent session (e.g., turn started,
- turn completed, session started).
- """
-
- object_type: Literal["AgentSystemMessage"] = "AgentSystemMessage"
- event_type: AgentSystemEventType
- session_id: str | None = Field(default=None, description="Session/thread identifier")
- error: str | None = Field(default=None, description="Error message for failed events")
- original_message: dict[str, Any] | None = Field(default=None, description="Original agent-specific message data")
-
-
-class AgentUsage(SerializableModel):
- """Normalized usage tracking across agents.
-
- Tracks token usage and costs in a unified format.
- """
-
- input_tokens: int | None = Field(default=None, description="Input/prompt tokens consumed")
- output_tokens: int | None = Field(default=None, description="Output/completion tokens generated")
- cached_tokens: int | None = Field(default=None, description="Cached input tokens reused")
- total_tokens: int | None = Field(default=None, description="Total tokens (input + output)")
- thinking_tokens: int | None = Field(default=None, description="Tokens used for extended thinking")
- total_cost_usd: float | None = Field(default=None, description="Estimated cost in USD")
-
-
-class AgentResultMessage(SerializableModel):
- """Result message with cost and usage information.
-
- Represents the final result of an agent session, including timing,
- usage statistics, and success/error status.
- """
-
- object_type: Literal["AgentResultMessage"] = "AgentResultMessage"
- session_id: str
- is_error: bool
- duration_ms: int | None = Field(default=None, description="Total duration in milliseconds")
- api_duration_ms: int | None = Field(default=None, description="API call duration in milliseconds")
- num_turns: int | None = Field(default=None, description="Number of conversation turns")
- usage: AgentUsage | None = Field(default=None, description="Token usage and cost information")
- result: str | None = Field(default=None, description="Final result or output from the agent")
- error: str | None = Field(default=None, description="Error message if is_error=True")
- original_message: dict[str, Any] | None = Field(default=None, description="Original agent-specific message data")
-
-
-AgentMessage = AgentUserMessage | AgentAssistantMessage | AgentSystemMessage | AgentResultMessage
-AgentMessageUnion = Annotated[
- Annotated[AgentUserMessage, Tag("AgentUserMessage")]
- | Annotated[AgentAssistantMessage, Tag("AgentAssistantMessage")]
- | Annotated[AgentSystemMessage, Tag("AgentSystemMessage")]
- | Annotated[AgentResultMessage, Tag("AgentResultMessage")],
- build_discriminator(),
-]
-
-
-class ToolUseRecord(SerializableModel):
- """A record of a tool use."""
-
- request_message: AgentToolUseBlock
- result_message: AgentToolResultBlock
-
- @property
- def tool_name(self) -> str:
- """The name of the tool used."""
- return self.request_message.name
-
- @property
- def tool_input(self) -> dict[str, Any]:
- """The input to the tool."""
- return self.request_message.input
-
-
-class AgentOptions(SerializableModel):
- """Parent class for all agent options."""
-
- cwd: str | Path | None = None
diff --git a/imbue_core/imbue_core/agents/agent_api/interaction.py b/imbue_core/imbue_core/agents/agent_api/interaction.py
@@ -1,96 +0,0 @@
-from typing import Sequence
-
-from imbue_core.agents.agent_api.data_types import AgentAssistantMessage
-from imbue_core.agents.agent_api.data_types import AgentMessage
-from imbue_core.agents.agent_api.data_types import AgentOptions
-from imbue_core.agents.agent_api.data_types import AgentToolResultBlock
-from imbue_core.agents.agent_api.data_types import AgentToolUseBlock
-from imbue_core.agents.agent_api.data_types import AgentUserMessage
-from imbue_core.agents.agent_api.data_types import ToolUseRecord
-from imbue_core.pydantic_serialization import SerializableModel
-
-
-class AgentInteraction:
- """A class for tracking an ongoing interaction with an agent.
-
- Note that this class is not thread-safe.
- """
-
- def __init__(self, prompt: str, options: AgentOptions) -> None:
- self.prompt = prompt
- self.options = options
- self.messages: list[AgentMessage] = []
- self.tool_use_records: list[ToolUseRecord] = []
- self._unresolved_tool_use_requests: list[AgentToolUseBlock] = []
-
- def put(self, message: AgentMessage) -> None:
- self.messages.append(message)
-
- if isinstance(message, AgentAssistantMessage):
- for assistant_content_block in message.content:
- if isinstance(assistant_content_block, AgentToolUseBlock):
- self._unresolved_tool_use_requests.append(assistant_content_block)
- elif isinstance(message, AgentUserMessage) and isinstance(message.content, list):
- for content_block in message.content:
- if isinstance(content_block, AgentToolResultBlock):
- remaining_unresolved_requests = []
- for request in self._unresolved_tool_use_requests:
- if request.id == content_block.tool_use_id:
- self.tool_use_records.append(
- ToolUseRecord(
- request_message=request,
- result_message=content_block,
- )
- )
- else:
- remaining_unresolved_requests.append(request)
- self._unresolved_tool_use_requests = remaining_unresolved_requests
-
- def find_tool_use_record_by_command(self, command: str, by_most_recent: bool = True) -> ToolUseRecord | None:
- """Look for tool use request and result messages by the tool command.
-
- If by_most_recent is True, the records are searched in reverse order (most recent first).
- """
- return _find_tool_use_record_by_command(self.tool_use_records, command, by_most_recent)
-
-
-class AgentInteractionRecord(SerializableModel):
- """A serializable record of a completed agent interaction.
-
- This is meant to be used for storing a completed log in a database or cache.
- """
-
- prompt: str
- options: AgentOptions
- messages: tuple[AgentMessage, ...]
- tool_use_records: tuple[ToolUseRecord, ...]
-
- @classmethod
- def from_agent_interaction(cls, agent_interaction: AgentInteraction) -> "AgentInteractionRecord":
- return cls(
- prompt=agent_interaction.prompt,
- options=agent_interaction.options,
- messages=tuple(agent_interaction.messages),
- tool_use_records=tuple(agent_interaction.tool_use_records),
- )
-
- def find_tool_use_record_by_command(self, command: str, by_most_recent: bool = True) -> ToolUseRecord | None:
- """Look for tool use request and result messages by the tool command.
-
- If by_most_recent is True, the records are searched in reverse order (most recent first).
- """
- return _find_tool_use_record_by_command(self.tool_use_records, command, by_most_recent)
-
-
-def _find_tool_use_record_by_command(
- tool_use_records: Sequence[ToolUseRecord], command: str, reverse: bool = True
-) -> ToolUseRecord | None:
- """Look for tool use request and result messages by the tool command.
-
- If reverse is True, the records are searched in reverse order (most recent first).
- """
- for record in reversed(tool_use_records) if reverse else tool_use_records:
- tool_input = record.tool_input
- if "command" in tool_input and tool_input["command"] == command:
- return record
- return None
diff --git a/imbue_core/imbue_core/agents/agent_api/transport.py b/imbue_core/imbue_core/agents/agent_api/transport.py
@@ -1,176 +0,0 @@
-import json
-import os
-import subprocess
-import threading
-from abc import ABC
-from abc import abstractmethod
-from contextlib import contextmanager
-from pathlib import Path
-from subprocess import PIPE
-from typing import Any
-from typing import ContextManager
-from typing import Generator
-from typing import Generic
-from typing import Iterable
-from typing import Iterator
-from typing import Self
-from typing import Sequence
-from typing import TypeVar
-
-from imbue_core.agents.agent_api.data_types import AgentOptions
-from imbue_core.agents.agent_api.errors import AgentCLIConnectionError
-from imbue_core.agents.agent_api.errors import (
- AgentCLIJSONDecodeError as SDKJSONDecodeError,
-)
-from imbue_core.agents.agent_api.errors import AgentCLINotFoundError
-from imbue_core.agents.agent_api.errors import AgentProcessError
-from imbue_core.pydantic_serialization import SerializableModel
-
-TransportOptionsT = TypeVar("TransportOptionsT", bound=SerializableModel)
-
-
-class AgentTransport(ABC, Generic[TransportOptionsT]):
- """Abstract transport for Agent communication."""
-
- @classmethod
- @abstractmethod
- def build(cls, options: TransportOptionsT) -> ContextManager[Self]:
- """Build a transport from options.
-
- This is the main entry point for building a transport and managing its lifecycle.
- """
-
- @abstractmethod
- def send_request(self, messages: list[Any], agent_options: AgentOptions) -> None:
- """Send request to underlying agent via transport."""
-
- @abstractmethod
- def receive_messages(self) -> Iterator[dict[str, Any]]:
- """Receive messages from underlying agent via transport."""
-
- @abstractmethod
- def is_connected(self) -> bool:
- """Check if transport is connected."""
-
-
-class AgentSubprocessCLITransportOptions(SerializableModel):
- """Options for AgentSubprocessCLITransport."""
-
- cmd: Sequence[str]
- cwd: str | Path | None = None
- extra_env_vars: dict[str, str] | None = None
-
-
-class AgentSubprocessCLITransport(AgentTransport[AgentSubprocessCLITransportOptions]):
- """Subprocess transport using Coding Agent via a CLI."""
-
- def __init__(
- self,
- popen: subprocess.Popen[str],
- ) -> None:
- self._process = popen
- self._stdin_stream = popen.stdin
- self._stdout_stream = popen.stdout
- self._stderr_stream = popen.stderr
-
- @classmethod
- @contextmanager
- def build(cls, options: AgentSubprocessCLITransportOptions) -> Generator[Self, None, None]:
- extra_env_vars = options.extra_env_vars or {}
- try:
- popen = subprocess.Popen(
- options.cmd,
- stdin=PIPE,
- stdout=PIPE,
- stderr=PIPE,
- cwd=options.cwd,
- env={**os.environ, **extra_env_vars},
- # ensure output is line buffered
- bufsize=1,
- text=True,
- encoding="utf-8",
- )
- except FileNotFoundError as e:
- raise AgentCLINotFoundError(f"Agent CLI not found for: cmd={options.cmd}") from e
- except Exception as e:
- raise AgentCLIConnectionError(f"Failed to start Agent CLI via cmd={options.cmd}: {e}") from e
-
- try:
- yield cls(popen)
- finally:
- # Make sure to terminate the process if it is still running, and clean up the streams
- if popen.poll() is None:
- try:
- popen.terminate()
- popen.wait(timeout=5.0)
- except subprocess.TimeoutExpired:
- popen.kill()
- popen.wait(timeout=5.0)
- popen.stdout and popen.stdout.close()
- popen.stderr and popen.stderr.close()
- popen.stdin and popen.stdin.close()
-
- def send_request(self, messages: Iterable[dict[str, Any] | str], agent_options: AgentOptions) -> None:
- process = self._process
- stdin_stream = self._stdin_stream
- if not process or not stdin_stream:
- raise AgentCLIConnectionError("Not connected")
-
- for message in messages:
- stdin_stream.write(json.dumps(message) + "\n")
- stdin_stream.flush()
-
- def _read_stderr(self, output_buffer: list[str]) -> None:
- """Read stderr in background."""
- stderr_stream = self._stderr_stream
- if stderr_stream:
- try:
- for line in stderr_stream:
- output_buffer.append(line.strip())
- except subprocess.SubprocessError:
- pass
-
- def receive_messages(self) -> Iterator[dict[str, Any]]:
- process = self._process
- stdout_stream = self._stdout_stream
- if not process or not stdout_stream:
- raise AgentCLIConnectionError("Not connected")
-
- stderr_lines: list[str] = []
- stderr_read_thread = threading.Thread(target=self._read_stderr, args=(stderr_lines,))
- stderr_read_thread.start()
-
- try:
- for line in stdout_stream:
- line_str = line.strip()
- if not line_str:
- continue
-
- try:
- data = json.loads(line_str)
- try:
- yield data
- except GeneratorExit:
- # Handle generator cleanup gracefully
- return
- except json.JSONDecodeError as e:
- if line_str.startswith("{") or line_str.startswith("["):
- raise SDKJSONDecodeError(line_str, e) from e
- continue
-
- except subprocess.SubprocessError:
- pass
-
- process.wait()
- if process.returncode is not None and process.returncode != 0:
- stderr_output = "\n".join(stderr_lines)
- if stderr_output and "error" in stderr_output.lower():
- raise AgentProcessError(
- "CLI process failed",
- exit_code=process.returncode,
- stderr=stderr_output,
- )
-
- def is_connected(self) -> bool:
- process = self._process
- return process is not None and process.returncode is None
diff --git a/imbue_core/imbue_core/agents/configs.py b/imbue_core/imbue_core/agents/configs.py
@@ -1,123 +0,0 @@
-from pathlib import Path
-from typing import Any
-from typing import assert_never
-
-from imbue_core.agents.llm_apis.anthropic_api import AnthropicModelName
-from imbue_core.agents.llm_apis.anthropic_api import count_anthropic_tokens
-from imbue_core.agents.llm_apis.common import get_model_max_context_length
-from imbue_core.agents.llm_apis.constants import approximate_token_count
-from imbue_core.agents.llm_apis.data_types import ModelStr
-from imbue_core.agents.llm_apis.mock_api import MY_MOCK_MODEL_INFO
-from imbue_core.agents.llm_apis.openai_api import OpenAIModelName
-from imbue_core.agents.llm_apis.openai_api import count_openai_tokens
-from imbue_core.language_model_mode import LanguageModelMode
-from imbue_core.pydantic_serialization import SerializableModel
-
-
-class LanguageModelGenerationConfig(SerializableModel):
- model_name: ModelStr = OpenAIModelName.GPT_4O_2024_08_06
- # this should almost always be None (you dont want to save your cache path into the hammer invocation data!)
- cache_path: Path | None = None
- count_tokens_cache_path: Path | None = None
- is_prompt_debugging_enabled: bool = False
-
- # If true, the LLM API will cache the inputs to the LLM call as well as the outputs, which makes prompt diffing easier.
- is_caching_inputs: bool = False
-
- # if this is set, the LLM API will return ONLY cached responses
- is_running_offline: bool = False
-
- # if set, the LLM API will return log probabilities for the output tokens (if supported by the model)
- is_using_logprobs: bool = False
-
- # Retry configuration
- retry_jitter_factor: float = 0.5
-
- def model_post_init(self, __context: Any) -> None:
- super().model_post_init(__context)
- # FIXME: do proper validation
- if self.cache_path is None and self.is_caching_inputs:
- raise ValueError("cache_path must be provided if is_caching_inputs is True")
-
- def count_tokens(self, text: str) -> int:
- """Count tokens in the given text using the model's tokenizer."""
- if self.model_name in (v for v in OpenAIModelName):
- return count_openai_tokens(text, self.model_name)
- if self.model_name in (v for v in AnthropicModelName):
- return count_anthropic_tokens(text)
- return approximate_token_count(text)
-
- def get_max_context_length(self) -> int:
- """Get the maximum context length for this model."""
- return get_model_max_context_length(self.model_name)
-
- def is_custom_model(self) -> bool:
- """Return True if this is a custom/user-defined model.
-
- Custom models use approximate token counting since there's no mechanism
- for defining a tokenizer for them.
- """
- return False
-
-
-class OpenAICompatibleModelConfig(LanguageModelGenerationConfig):
- """Configuration for custom models using OpenAI-compatible APIs (e.g., Ollama, local LLMs)."""
-
- custom_base_url: str
- custom_api_key_env: str
- custom_context_window: int
- custom_max_output_tokens: int
-
- def count_tokens(self, text: str) -> int:
- """Count tokens using approximation since we don't have access to the model's tokenizer."""
- return approximate_token_count(text)
-
- def get_max_context_length(self) -> int:
- """Get the maximum context length for this model."""
- return self.custom_context_window
-
- def is_custom_model(self) -> bool:
- """Return True if this is a custom/user-defined model.
-
- Custom models use approximate token counting since there's no mechanism
- for defining a tokenizer for them.
- """
- # TODO: Support custom tokenizers with custom models.
- return True
-
-
-class MockedLanguageModelGenerationConfig(LanguageModelGenerationConfig):
- model_name: ModelStr = MY_MOCK_MODEL_INFO.model_name
- is_running_offline: bool = True
- mock_responses_path: Path
-
-
-def create_safe_llm_config(
- llm_name: ModelStr, mode: LanguageModelMode, cache_path: Path | None = None
-) -> LanguageModelGenerationConfig:
- match mode:
- case LanguageModelMode.LIVE:
- assert cache_path is None
- language_model_config = LanguageModelGenerationConfig(model_name=llm_name)
- case LanguageModelMode.OFFLINE:
- assert cache_path is not None
- language_model_config = LanguageModelGenerationConfig(
- model_name=llm_name,
- is_running_offline=True,
- is_caching_inputs=True,
- cache_path=cache_path,
- )
- case LanguageModelMode.UPDATE_SNAPSHOT:
- assert cache_path is not None
- language_model_config = LanguageModelGenerationConfig(
- model_name=llm_name, is_caching_inputs=True, cache_path=cache_path
- )
- case LanguageModelMode.MOCKED:
- assert cache_path is not None
- language_model_config = MockedLanguageModelGenerationConfig(
- model_name=llm_name, mock_responses_path=cache_path
- )
- case _ as unreachable:
- assert_never(unreachable) # pyre-ignore[6]: pyre doesn't understand enums
- assert False # because pyre doesn't really understand assert_never, either
- return language_model_config
diff --git a/imbue_core/imbue_core/agents/llm_apis/anthropic_api.py b/imbue_core/imbue_core/agents/llm_apis/anthropic_api.py
@@ -1,818 +0,0 @@
-import enum
-import inspect
-from contextlib import contextmanager
-from functools import lru_cache
-from pathlib import Path
-from types import FrameType
-from typing import AsyncGenerator
-from typing import Final
-from typing import Iterator
-
-import anthropic
-import httpx
-from anthropic._types import NOT_GIVEN
-from anthropic.types import CacheControlEphemeralParam
-from anthropic.types import MessageParam
-from anthropic.types import TextBlockParam
-from loguru import logger
-from pydantic.functional_validators import field_validator
-import tiktoken
-
-from imbue_core.agents.llm_apis.anthropic_data_types import AnthropicCachingInfo
-from imbue_core.agents.llm_apis.anthropic_data_types import AnthropicModelInfo
-from imbue_core.agents.llm_apis.api_utils import convert_prompt_to_messages
-from imbue_core.agents.llm_apis.api_utils import (
- create_costed_language_model_response_for_single_result,
-)
-from imbue_core.agents.llm_apis.data_types import CachedCountTokensResponse
-from imbue_core.agents.llm_apis.data_types import CachingInfo
-from imbue_core.agents.llm_apis.data_types import CostedLanguageModelResponse
-from imbue_core.agents.llm_apis.data_types import CountTokensInputs
-from imbue_core.agents.llm_apis.data_types import CountTokensResponse
-from imbue_core.agents.llm_apis.data_types import LanguageModelGenerationParams
-from imbue_core.agents.llm_apis.data_types import LanguageModelResponseUsage
-from imbue_core.agents.llm_apis.data_types import ResponseStopReason
-from imbue_core.agents.llm_apis.errors import BadAPIRequestError
-from imbue_core.agents.llm_apis.errors import LanguageModelInvalidModelNameError
-from imbue_core.agents.llm_apis.errors import MissingAPIKeyError
-from imbue_core.agents.llm_apis.errors import NewSeedRetriableLanguageModelError
-from imbue_core.agents.llm_apis.errors import SafelyRetriableTransientLanguageModelError
-from imbue_core.agents.llm_apis.errors import TransientLanguageModelError
-from imbue_core.agents.llm_apis.errors import UnsetCachePathError
-from imbue_core.agents.llm_apis.language_model_api import LanguageModelAPI
-from imbue_core.agents.llm_apis.models import ModelInfo
-from imbue_core.agents.llm_apis.stream import LanguageModelStreamDeltaEvent
-from imbue_core.agents.llm_apis.stream import LanguageModelStreamEndEvent
-from imbue_core.agents.llm_apis.stream import LanguageModelStreamEvent
-from imbue_core.agents.llm_apis.stream import LanguageModelStreamStartEvent
-from imbue_core.async_monkey_patches import log_exception
-from imbue_core.caching import AsyncCache
-from imbue_core.frozen_utils import FrozenDict
-from imbue_core.frozen_utils import FrozenMapping
-from imbue_core.itertools import only
-from imbue_core.nested_evolver import assign
-from imbue_core.nested_evolver import chill
-from imbue_core.nested_evolver import evolver
-from imbue_core.secrets_utils import get_secret
-
-
-class AnthropicModelName(enum.StrEnum):
- CLAUDE_3_HAIKU_2024_03_07 = "claude-3-haiku-20240307"
- CLAUDE_3_OPUS_2024_02_29 = "claude-3-opus-20240229"
- CLAUDE_3_5_SONNET_2024_06_20 = "claude-3-5-sonnet-20240620"
- CLAUDE_3_5_SONNET_2024_10_22 = "claude-3-5-sonnet-20241022"
- CLAUDE_3_5_HAIKU_2024_10_22 = "claude-3-5-haiku-20241022"
- CLAUDE_3_7_SONNET_2025_02_19 = "claude-3-7-sonnet-20250219"
- CLAUDE_4_OPUS_2025_05_14 = "claude-opus-4-20250514"
- CLAUDE_4_1_OPUS_2025_08_05 = "claude-opus-4-1-20250805"
- CLAUDE_4_SONNET_2025_05_14 = "claude-sonnet-4-20250514"
- CLAUDE_4_5_SONNET_2025_09_29 = "claude-sonnet-4-5-20250929"
- CLAUDE_4_5_HAIKU_2025_10_01 = "claude-haiku-4-5-20251001"
- CLAUDE_4_5_OPUS_2025_11_01 = "claude-opus-4-5-20251101"
- # the same as above but with the token limit and cost per token for the 1M token limit
- # TODO: combine these and add ability for token costs to be nonlinear
- # FIXME: this is an exception where the model name is not the same as the model name in the API
- CLAUDE_4_SONNET_2025_05_14_LONG = "claude-sonnet-4-20250514-long"
- CLAUDE_4_5_SONNET_2025_09_29_LONG = "claude-sonnet-4-5-20250929-long"
-
- # the following are 'retired' and are no longer available: https://docs.claude.com/en/docs/about-claude/model-deprecations
- # CLAUDE_2_1 = "claude-2.1"
- # CLAUDE_2 = "claude-2"
- # CLAUDE_3_SONNET_2024_02_29 = "claude-3-sonnet-20240229"
-
-
-# Basic info is available at https://docs.anthropic.com/claude/reference/models
-# Rate limits for Anthropic models are available on our dashboard: https://console.anthropic.com/settings/limits
-# (we have a custom plan, so the public docs don't reflect our actual rate limits)
-# Prompt caching pricing is available at https://docs.claude.com/en/docs/build-with-claude/prompt-caching#pricing
-# NOTE: as of 2025-06-04, there are some models that don't have rate limits set in our dashboard
-ANTHROPIC_MODEL_INFO_BY_NAME: FrozenMapping[AnthropicModelName, ModelInfo] = FrozenDict(
- {
- AnthropicModelName.CLAUDE_3_HAIKU_2024_03_07: ModelInfo(
- model_name=AnthropicModelName.CLAUDE_3_HAIKU_2024_03_07,
- cost_per_input_token=0.25 / 1_000_000,
- cost_per_output_token=1.25 / 1_000_000,
- max_input_tokens=200_000,
- max_output_tokens=4096,
- rate_limit_req=4000 / 60, # 4000 RPM = 66.67 RPS
- rate_limit_tok=4_000_000 / 60,
- rate_limit_output_tok=800_000 / 60,
- provider_specific_info=AnthropicModelInfo(
- cost_per_5m_cache_write_token=0.3 / 1_000_000,
- cost_per_1h_cache_write_token=0.5 / 1_000_000,
- cost_per_cache_read_token=0.03 / 1_000_000,
- ),
- ),
- AnthropicModelName.CLAUDE_3_OPUS_2024_02_29: ModelInfo(
- model_name=AnthropicModelName.CLAUDE_3_OPUS_2024_02_29,
- cost_per_input_token=15.00 / 1_000_000,
- cost_per_output_token=75.00 / 1_000_000,
- max_input_tokens=200_000,
- max_output_tokens=4096,
- rate_limit_req=4000 / 60, # 4000 RPM = 66.67 RPS
- rate_limit_tok=1_000_000 / 60,
- rate_limit_output_tok=150_000 / 60,
- provider_specific_info=AnthropicModelInfo(
- cost_per_5m_cache_write_token=18.75 / 1_000_000,
- cost_per_1h_cache_write_token=30 / 1_000_000,
- cost_per_cache_read_token=1.5 / 1_000_000,
- ),
- ),
- AnthropicModelName.CLAUDE_3_5_SONNET_2024_06_20: ModelInfo(
- model_name=AnthropicModelName.CLAUDE_3_5_SONNET_2024_06_20,
- cost_per_input_token=3.00 / 1_000_000,
- cost_per_output_token=15.00 / 1_000_000,
- max_input_tokens=200_000,
- max_output_tokens=4096,
- rate_limit_req=5000 / 60, # 5000 RPM = 83.33 RPS
- rate_limit_tok=8_000_000 / 60,
- rate_limit_output_tok=1_600_000 / 60,
- provider_specific_info=AnthropicModelInfo(
- cost_per_5m_cache_write_token=3.75 / 1_000_000,
- cost_per_1h_cache_write_token=6 / 1_000_000,
- cost_per_cache_read_token=0.3 / 1_000_000,
- ),
- ),
- AnthropicModelName.CLAUDE_3_5_SONNET_2024_10_22: ModelInfo(
- model_name=AnthropicModelName.CLAUDE_3_5_SONNET_2024_10_22,
- cost_per_input_token=3.00 / 1_000_000,
- cost_per_output_token=15.00 / 1_000_000,
- max_input_tokens=200_000,
- max_output_tokens=8192,
- rate_limit_req=5000 / 60, # 5000 RPM = 83.33 RPS
- rate_limit_tok=8_000_000 / 60,
- rate_limit_output_tok=400_000 / 60,
- ),
- AnthropicModelName.CLAUDE_3_5_HAIKU_2024_10_22: ModelInfo(
- model_name=AnthropicModelName.CLAUDE_3_5_HAIKU_2024_10_22,
- cost_per_input_token=1.00 / 1_000_000,
- cost_per_output_token=5.00 / 1_000_000,
- max_input_tokens=200_000,
- max_output_tokens=8192,
- rate_limit_req=4000 / 60, # 4000 RPM = 66.67 RPS
- rate_limit_tok=4_000_000 / 60,
- rate_limit_output_tok=800_000 / 60,
- provider_specific_info=AnthropicModelInfo(
- cost_per_5m_cache_write_token=1 / 1_000_000,
- cost_per_1h_cache_write_token=1.6 / 1_000_000,
- cost_per_cache_read_token=0.08 / 1_000_000,
- ),
- ),
- AnthropicModelName.CLAUDE_3_7_SONNET_2025_02_19: ModelInfo(
- model_name=AnthropicModelName.CLAUDE_3_7_SONNET_2025_02_19,
- cost_per_input_token=3.00 / 1_000_000,
- cost_per_output_token=15.00 / 1_000_000,
- max_input_tokens=200_000,
- max_output_tokens=8192,
- rate_limit_req=None, # Currently no limit set in our dashboard
- rate_limit_tok=2_000_000 / 60,
- rate_limit_output_tok=400_000 / 60,
- provider_specific_info=AnthropicModelInfo(
- cost_per_5m_cache_write_token=3.75 / 1_000_000,
- cost_per_1h_cache_write_token=6 / 1_000_000,
- cost_per_cache_read_token=0.3 / 1_000_000,
- ),
- ),
- AnthropicModelName.CLAUDE_4_OPUS_2025_05_14: ModelInfo(
- model_name=AnthropicModelName.CLAUDE_4_OPUS_2025_05_14,
- cost_per_input_token=15.00 / 1_000_000,
- cost_per_output_token=75.00 / 1_000_000,
- max_input_tokens=200_000,
- max_output_tokens=32_000,
- rate_limit_req=4000 / 60,
- rate_limit_tok=2_000_000 / 60,
- rate_limit_output_tok=400_000 / 60,
- provider_specific_info=AnthropicModelInfo(
- cost_per_5m_cache_write_token=18.75 / 1_000_000,
- cost_per_1h_cache_write_token=30 / 1_000_000,
- cost_per_cache_read_token=1.5 / 1_000_000,
- ),
- ),
- AnthropicModelName.CLAUDE_4_1_OPUS_2025_08_05: ModelInfo(
- model_name=AnthropicModelName.CLAUDE_4_1_OPUS_2025_08_05,
- cost_per_input_token=15.00 / 1_000_000,
- cost_per_output_token=75.00 / 1_000_000,
- max_input_tokens=200_000,
- max_output_tokens=32_000,
- rate_limit_req=4000 / 60,
- rate_limit_tok=2_000_000 / 60,
- rate_limit_output_tok=400_000 / 60,
- provider_specific_info=AnthropicModelInfo(
- cost_per_5m_cache_write_token=18.75 / 1_000_000,
- cost_per_1h_cache_write_token=30 / 1_000_000,
- cost_per_cache_read_token=1.5 / 1_000_000,
- ),
- ),
- AnthropicModelName.CLAUDE_4_5_OPUS_2025_11_01: ModelInfo(
- model_name=AnthropicModelName.CLAUDE_4_5_OPUS_2025_11_01,
- cost_per_input_token=5.00 / 1_000_000,
- cost_per_output_token=25.00 / 1_000_000,
- max_input_tokens=200_000,
- max_output_tokens=64_000,
- rate_limit_req=4000 / 60,
- rate_limit_tok=2_000_000 / 60,
- rate_limit_output_tok=400_000 / 60,
- provider_specific_info=AnthropicModelInfo(
- cost_per_5m_cache_write_token=6.25 / 1_000_000,
- cost_per_1h_cache_write_token=10 / 1_000_000,
- cost_per_cache_read_token=0.5 / 1_000_000,
- ),
- ),
- AnthropicModelName.CLAUDE_4_SONNET_2025_05_14: ModelInfo(
- model_name=AnthropicModelName.CLAUDE_4_SONNET_2025_05_14,
- cost_per_input_token=3.00 / 1_000_000,
- cost_per_output_token=15.00 / 1_000_000,
- max_input_tokens=200_000,
- max_output_tokens=64_000,
- rate_limit_req=None, # Currently no limit set in our dashboard
- rate_limit_tok=2_000_000 / 60,
- rate_limit_output_tok=400_000 / 60,
- provider_specific_info=AnthropicModelInfo(
- cost_per_5m_cache_write_token=3.75 / 1_000_000,
- cost_per_1h_cache_write_token=6 / 1_000_000,
- cost_per_cache_read_token=0.3 / 1_000_000,
- ),
- ),
- AnthropicModelName.CLAUDE_4_5_SONNET_2025_09_29: ModelInfo(
- model_name=AnthropicModelName.CLAUDE_4_5_SONNET_2025_09_29,
- cost_per_input_token=3.00 / 1_000_000,
- cost_per_output_token=15.00 / 1_000_000,
- max_input_tokens=200_000,
- max_output_tokens=64_000,
- rate_limit_req=None, # Currently no limit set in our dashboard
- rate_limit_tok=2_000_000 / 60,
- rate_limit_output_tok=400_000 / 60,
- provider_specific_info=AnthropicModelInfo(
- cost_per_5m_cache_write_token=3.75 / 1_000_000,
- cost_per_1h_cache_write_token=6 / 1_000_000,
- cost_per_cache_read_token=0.3 / 1_000_000,
- ),
- ),
- AnthropicModelName.CLAUDE_4_5_HAIKU_2025_10_01: ModelInfo(
- model_name=AnthropicModelName.CLAUDE_4_5_HAIKU_2025_10_01,
- cost_per_input_token=1.00 / 1_000_000,
- cost_per_output_token=5.00 / 1_000_000,
- max_input_tokens=200_000,
- max_output_tokens=64_000,
- rate_limit_req=4_000 / 60,
- rate_limit_tok=4_000_000 / 60,
- rate_limit_output_tok=800_000 / 60,
- provider_specific_info=AnthropicModelInfo(
- cost_per_5m_cache_write_token=1.25 / 1_000_000,
- cost_per_1h_cache_write_token=2.0 / 1_000_000,
- cost_per_cache_read_token=0.1 / 1_000_000,
- ),
- ),
- AnthropicModelName.CLAUDE_4_SONNET_2025_05_14_LONG: ModelInfo(
- model_name=AnthropicModelName.CLAUDE_4_SONNET_2025_05_14_LONG,
- # the first 200_000 input tokens use the rates above, and the next up to 800_000 use the rate 6.0 / 1_000_000.
- # thus the maximum average cost per input token is (3.0 * 200_000 + 6.0 * 800_000) / 1_000_000 = 5.4 per 1_000_000.
- # (all output tokens may be past 200_000 input tokens, so the max average cost there is just the cost for tokens after 200_000)
- cost_per_input_token=5.40 / 1_000_000,
- cost_per_output_token=22.50 / 1_000_000,
- max_input_tokens=1_000_000,
- max_output_tokens=64_000,
- rate_limit_req=None, # Currently no limit set in our dashboard
- rate_limit_tok=1_000_000 / 60, # <-- yeah they let us have one (1) 1M request per minute
- rate_limit_output_tok=200_000 / 60,
- ),
- AnthropicModelName.CLAUDE_4_5_SONNET_2025_09_29_LONG: ModelInfo(
- model_name=AnthropicModelName.CLAUDE_4_5_SONNET_2025_09_29_LONG,
- # the first 200_000 input tokens use the rates above, and the next up to 800_000 use the rate 6.0 / 1_000_000.
- # thus the maximum average cost per input token is (3.0 * 200_000 + 6.0 * 800_000) / 1_000_000 = 5.4 per 1_000_000.
- # (all output tokens may be past 200_000 input tokens, so the max average cost there is just the cost for tokens after 200_000)
- cost_per_input_token=5.40 / 1_000_000,
- cost_per_output_token=22.50 / 1_000_000,
- max_input_tokens=1_000_000,
- max_output_tokens=64_000,
- rate_limit_req=None, # Currently no limit set in our dashboard
- rate_limit_tok=1_000_000 / 60, # <-- yeah they let us have one (1) 1M request per minute
- rate_limit_output_tok=200_000 / 60,
- ),
- }
-)
-
-
-_ROLE_TO_ANTHROPIC_ROLE: Final[FrozenMapping[str, str]] = FrozenDict(
- {
- "HUMAN": "user",
- "ASSISTANT": "assistant",
- "USER": "user",
- "USER_CACHED": "user",
- "SYSTEM": "system",
- "SYSTEM_CACHED": "system",
- }
-)
-
-_ANTHROPIC_STOP_REASON_TO_STOP_REASON: Final[FrozenMapping[str, ResponseStopReason]] = FrozenDict(
- {
- "end_turn": ResponseStopReason.END_TURN,
- "max_tokens": ResponseStopReason.MAX_TOKENS,
- "stop_sequence": ResponseStopReason.STOP_SEQUENCE,
- "refusal": ResponseStopReason.CONTENT_FILTER,
- }
-)
-
-_ANTHROPIC_BETA_PROMPT_CACHING = "prompt-caching-2024-07-31"
-_ANTHROPIC_BETA_OAUTH = "oauth-2025-04-20"
-
-
-@lru_cache(maxsize=1)
-def get_anthropic_tokenizer() -> tiktoken.Encoding:
- """Use cl100k_base encoding as an approximation for Claude tokenization.
-
- Modern Anthropic SDK does not expose a tokenizer directly and instead
- relies on API calls to count tokens. Using that implementation would
- put HTTP calls in our `count_tokens` implementation which would be tricky
- as the method would have to be async or block the event loop.
-
- Instead, we use tiktoken's cl100k_base encoding (used by GPT-4) as a
- reasonable approximation. This allows us to count tokens without making
- HTTP requests at the cost of slightly inaccurate token counts.
- """
- return tiktoken.get_encoding("cl100k_base")
-
-
-def count_anthropic_tokens(text: str) -> int:
- return int(len(get_anthropic_tokenizer().encode(text)) * 1.1)
-
-
-SystemMessageParam = TextBlockParam
-
-
-def _convert_prompt_to_anthropic_messages(
- prompt: str,
-) -> tuple[list[MessageParam], list[SystemMessageParam] | None]:
- """Converts a prompt into list of non-system (user/assistant) messages and the optional system prompt."""
- non_system_messages = []
- system_messages = []
- for msg in convert_prompt_to_messages(prompt, is_cache_role_preserved=True):
- role = _ROLE_TO_ANTHROPIC_ROLE[msg.role]
- if msg.role == "SYSTEM_CACHED":
- system_messages.append(
- {
- "type": "text",
- "text": msg.content,
- "cache_control": {"type": "ephemeral"},
- },
- )
- elif role == "system":
- system_messages.append(
- {
- "type": "text",
- "text": msg.content,
- },
- )
- elif role == "USER_CACHED":
- non_system_messages.append(
- MessageParam( # pyre-fixme[28]: MessageParam doesn't have cache_control
- content=msg.content,
- role="user",
- cache_control=CacheControlEphemeralParam(type="ephemeral"),
- )
- )
- else:
- non_system_messages.append(MessageParam(content=msg.content, role=role)) # type: ignore
-
- if len(system_messages) > 1:
- logger.debug("system_messages: {}", system_messages)
- raise ValueError(f"Anthropic API supports only 0 or 1 system message; got {len(system_messages)}.")
-
- if len(non_system_messages) == 0:
- system_messages = None
-
- return non_system_messages, system_messages
-
-
-@contextmanager
-def _anthropic_exception_manager() -> Iterator[None]:
- """Simple context manager for parsing Anthropic API exceptions."""
- # ref
- try:
- yield
- except anthropic.InternalServerError as e:
- # this can be caused by either malformed requests or transient errors, so play it safe and retry
- raise TransientLanguageModelError(str(e)) from e
- except anthropic.BadRequestError as e:
- logger.info("BadAPIRequestError {e}", e=e)
- raise BadAPIRequestError(str(e)) from e
- except TypeError as e:
- logger.info("Type error calling Anthropic API: {e}", e=e)
- raise BadAPIRequestError(str(e)) from e
- except anthropic.APIConnectionError as e:
- raise TransientLanguageModelError(str(e)) from e
- except anthropic.RateLimitError as e:
- extra_header_keys = [x for x in e.response.headers.keys() if x.startswith("anthropic-")]
- extra_data = ", ".join([f"{key}={e.response.headers[key]}" for key in extra_header_keys])
- extra_info = f"Rate limit data: {extra_data}"
- raise TransientLanguageModelError(extra_info) from e
- except anthropic.APIStatusError as e:
- if "overloaded_error" in str(e):
- raise SafelyRetriableTransientLanguageModelError(str(e)) from e
- if "internal server error" in str(e).lower():
- raise SafelyRetriableTransientLanguageModelError(str(e)) from e
- # this happens when anthropic provides us open source code and then feels bad about it
- # anthropic.APIStatusError: {'type': 'error', 'error': {'details': None, 'type': 'invalid_request_error', 'message': 'Output blocked by content filtering policy'}}
- if e.message == "Output blocked by content filtering policy":
- raise NewSeedRetriableLanguageModelError(e)
- logger.info(str(e))
- if e.status_code == 409 or e.status_code >= 500:
- raise TransientLanguageModelError(str(e)) from e
- raise
- except httpx.RemoteProtocolError as e:
- logger.info(str(e))
- raise TransientLanguageModelError("httpx.RemoteProtocolError") from e
- except (BadAPIRequestError, TransientLanguageModelError, MissingAPIKeyError):
- # we already raised this error ourselves earlier, so we don't need to mark it as unknown
- raise
- except Exception as e:
- # we catch TransientLanguageModelError later to retry it, but we still want to log it so it's not silent
- log_exception(
- e,
- "Failed to generate output from Anthropic, unknown error of type {type_name}",
- type_name=type(e).__name__,
- )
- raise TransientLanguageModelError("Unknown error") from e
-
-
-class MissingCachingInfoError(Exception):
- pass
-
-
-class AnthropicAPI(LanguageModelAPI):
- model_name: AnthropicModelName = AnthropicModelName.CLAUDE_4_SONNET_2025_05_14
- is_conversational: bool = True
-
- # Anthropic specific args
- # unclear what the timeout ought to be actually, set to 1 minute for now because their default of 10 minutes seems insane
- timeout: float = 60.0
- max_retries: int = 0
- count_tokens_cache_path: Path | None = None
-
- @field_validator("model_name") # pyre-ignore[56]: pyre doesn't understand pydantic
- @classmethod
- def validate_model_name(cls, v: str) -> str:
- if v not in ANTHROPIC_MODEL_INFO_BY_NAME:
- raise LanguageModelInvalidModelNameError(v, cls.__name__, list(ANTHROPIC_MODEL_INFO_BY_NAME))
- return v
-
- @property
- def model_info(self) -> ModelInfo:
- return ANTHROPIC_MODEL_INFO_BY_NAME[self.model_name]
-
- def _get_sync_client(self) -> anthropic.Anthropic:
- api_key, auth_token = _get_api_key_or_auth_token()
- if api_key:
- return anthropic.Anthropic(api_key=api_key)
- else:
- return anthropic.Anthropic(
- auth_token=auth_token,
- default_headers={"anthropic-beta": _ANTHROPIC_BETA_OAUTH},
- )
-
- def _get_client(self) -> anthropic.AsyncAnthropic:
- api_key, auth_token = _get_api_key_or_auth_token()
- if api_key:
- return anthropic.AsyncAnthropic(
- api_key=api_key,
- max_retries=self.max_retries,
- timeout=self.timeout,
- default_headers={"anthropic-beta": _ANTHROPIC_BETA_PROMPT_CACHING},
- )
- else:
- return anthropic.AsyncAnthropic(
- auth_token=auth_token,
- max_retries=self.max_retries,
- timeout=self.timeout,
- default_headers={"anthropic-beta": f"{_ANTHROPIC_BETA_PROMPT_CACHING},{_ANTHROPIC_BETA_OAUTH}"},
- )
-
- async def _call_api(
- self,
- prompt: str,
- params: LanguageModelGenerationParams,
- network_failure_count: int = 0,
- ) -> CostedLanguageModelResponse:
- assert (
- params.count == 1
- ), "Anthropic API only supports count=1. It is possible to hack around this by using a for loop, but doesn't seem worth it right now."
-
- non_system_messages, system_messages = _convert_prompt_to_anthropic_messages(prompt)
-
- with _anthropic_exception_manager():
- async with self._get_client() as client:
- if params.max_tokens is None:
- # NOTE: anthropic's API REQUIRES you to provide this, if you don't pass it in we just set it to the maximum possible
-
- # use the evolver method of updating instead
- # params.max_tokens = self.model_info.max_output_tokens
- param_with_max_tokens_evolver = evolver(params)
- assign(
- param_with_max_tokens_evolver.max_tokens,
- lambda: self.model_info.max_output_tokens,
- )
- params = chill(param_with_max_tokens_evolver)
- assert params.max_tokens is not None, "max_tokens must be provided for Anthropic API"
-
- if self.model_name in (
- AnthropicModelName.CLAUDE_4_5_SONNET_2025_09_29_LONG,
- AnthropicModelName.CLAUDE_4_SONNET_2025_05_14_LONG,
- ):
- # FIXME: Fix this once this is no longer beta or as this becomes required for more models
- # Map the name back to the actual model name for the API call
- if self.model_name == AnthropicModelName.CLAUDE_4_5_SONNET_2025_09_29_LONG:
- model_name = AnthropicModelName.CLAUDE_4_5_SONNET_2025_09_29
- elif self.model_name == AnthropicModelName.CLAUDE_4_SONNET_2025_05_14_LONG:
- model_name = AnthropicModelName.CLAUDE_4_SONNET_2025_05_14
- else:
- assert False, "unreachable"
- api_result = await client.beta.messages.create(
- messages=non_system_messages,
- stop_sequences=([params.stop] if params.stop is not None else NOT_GIVEN),
- model=model_name,
- temperature=params.temperature,
- system=prepend_claude_code_system_prompt(system_messages),
- max_tokens=params.max_tokens,
- betas=["context-1m-2025-08-07"],
- )
- detailed_caching_data = AnthropicCachingInfo(
- written_5m=api_result.usage.cache_creation.ephemeral_5m_input_tokens,
- written_1h=api_result.usage.cache_creation.ephemeral_1h_input_tokens,
- )
- else:
- api_result = await client.messages.create(
- messages=non_system_messages,
- stop_sequences=([params.stop] if params.stop is not None else NOT_GIVEN),
- model=self.model_name,
- temperature=params.temperature,
- system=prepend_claude_code_system_prompt(system_messages),
- max_tokens=params.max_tokens,
- )
- detailed_caching_data = AnthropicCachingInfo(
- written_5m=api_result.usage.cache_creation.ephemeral_5m_input_tokens,
- written_1h=api_result.usage.cache_creation.ephemeral_1h_input_tokens,
- )
- text = only(api_result.content).text
- if api_result.stop_reason:
- stop_reason = _ANTHROPIC_STOP_REASON_TO_STOP_REASON.get(
- str(api_result.stop_reason), ResponseStopReason.NONE
- )
- else:
- stop_reason = ResponseStopReason.NONE
- if params.stop and stop_reason == ResponseStopReason.STOP_SEQUENCE:
- text += params.stop
- logger.trace(text)
-
- prompt_tokens = api_result.usage.input_tokens
- completion_tokens = api_result.usage.output_tokens # type: ignore
- caching_info = CachingInfo(
- read_from_cache=api_result.usage.cache_read_input_tokens,
- provider_specific_data=detailed_caching_data,
- )
- dollars_used = self.calculate_cost(prompt_tokens, completion_tokens, caching_info)
- logger.trace("Dollars used: {dollars_used}", dollars_used=dollars_used)
-
- return create_costed_language_model_response_for_single_result(
- text=text,
- prompt_tokens=prompt_tokens,
- completion_tokens=completion_tokens,
- stop_reason=stop_reason,
- network_failure_count=network_failure_count,
- dollars_used=dollars_used,
- caching_info=caching_info,
- )
-
- async def _get_api_stream(
- self,
- prompt: str,
- params: LanguageModelGenerationParams,
- ) -> AsyncGenerator[LanguageModelStreamEvent, None]:
- non_system_messages, system_messages = _convert_prompt_to_anthropic_messages(prompt)
- with _anthropic_exception_manager():
- async with self._get_client() as client:
- yield LanguageModelStreamStartEvent()
-
- # NOTE: anthropic's API REQUIRES you to provide this, if you don't pass it in we just set it to the maximum possible
- max_tokens = params.max_tokens if params.max_tokens is not None else self.model_info.max_output_tokens
- assert max_tokens is not None, "max_tokens must be provided for Anthropic API"
-
- if self.model_name in (
- AnthropicModelName.CLAUDE_4_5_SONNET_2025_09_29_LONG,
- AnthropicModelName.CLAUDE_4_SONNET_2025_05_14_LONG,
- ):
- # FIXME: Fix this once this is no longer beta or as this becomes required for more models
- # Map the name back to the actual model name for the API call
- if self.model_name == AnthropicModelName.CLAUDE_4_5_SONNET_2025_09_29_LONG:
- model_name = AnthropicModelName.CLAUDE_4_5_SONNET_2025_09_29
- elif self.model_name == AnthropicModelName.CLAUDE_4_SONNET_2025_05_14_LONG:
- model_name = AnthropicModelName.CLAUDE_4_SONNET_2025_05_14
- else:
- assert False, "unreachable"
- stream_fn = lambda **kwargs: client.beta.messages.stream(**kwargs, betas=["context-1m-2025-08-07"])
- cache_info_maker = lambda api_result: AnthropicCachingInfo(
- written_5m=api_result.usage.cache_creation.ephemeral_5m_input_tokens,
- written_1h=api_result.usage.cache_creation.ephemeral_1h_input_tokens,
- )
- else:
- model_name = self.model_name
- stream_fn = lambda **kwargs: client.messages.stream(**kwargs)
- cache_info_maker = lambda api_result: AnthropicCachingInfo(
- written_5m=api_result.usage.cache_creation.ephemeral_5m_input_tokens,
- written_1h=api_result.usage.cache_creation.ephemeral_1h_input_tokens,
- )
- async with stream_fn(
- max_tokens=max_tokens,
- messages=non_system_messages,
- model=model_name,
- stop_sequences=([params.stop] if params.stop is not None else NOT_GIVEN),
- system=system_messages or NOT_GIVEN,
- temperature=params.temperature,
- ) as stream:
- async for text_delta in stream.text_stream:
- yield LanguageModelStreamDeltaEvent(delta=text_delta)
-
- final_message = await stream.get_final_message()
- text = only(final_message.content).text
- stop_reason = (
- final_message.stop_reason if final_message.stop_reason is not None else ResponseStopReason.NONE
- )
- if params.stop and stop_reason == ResponseStopReason.STOP_SEQUENCE:
- yield LanguageModelStreamDeltaEvent(delta=params.stop)
- text += params.stop
- logger.trace(text)
-
- prompt_tokens = final_message.usage.input_tokens
- # useful to confirm that the cache is actually being hit
- logger.debug(
- "Used this many cached read tokens: {cached_tokens}",
- cached_tokens=final_message.usage.cache_read_input_tokens,
- )
- completion_tokens = final_message.usage.output_tokens
- caching_info = CachingInfo(
- read_from_cache=final_message.usage.cache_read_input_tokens,
- provider_specific_data=cache_info_maker(final_message),
- )
- dollars_used = self.calculate_cost(prompt_tokens, completion_tokens, caching_info)
-
- if final_message.stop_reason:
- stop_reason = _ANTHROPIC_STOP_REASON_TO_STOP_REASON.get(
- str(final_message.stop_reason), ResponseStopReason.NONE
- )
- else:
- stop_reason = ResponseStopReason.NONE
-
- logger.trace("Dollars used: {dollars_used}", dollars_used=dollars_used)
- yield LanguageModelStreamEndEvent(
- usage=LanguageModelResponseUsage(
- prompt_tokens_used=prompt_tokens,
- completion_tokens_used=completion_tokens,
- dollars_used=dollars_used,
- caching_info=caching_info,
- ),
- stop_reason=stop_reason,
- )
-
- def count_tokens(self, text: str) -> int:
- return count_anthropic_tokens(text)
-
- def get_count_tokens_response_cache(self) -> AsyncCache[CachedCountTokensResponse]:
- if self.count_tokens_cache_path is None:
- raise UnsetCachePathError()
- return AsyncCache(self.count_tokens_cache_path, CachedCountTokensResponse)
-
- async def check_count_tokens_cache(self, cache_key: str) -> CountTokensResponse | None:
- return await self.check_cache_core(self.get_count_tokens_response_cache, cache_key)
-
- async def _get_from_count_tokens_cache(
- self, frame: FrameType | None
- ) -> tuple[str | None, CountTokensResponse | None]:
- return await self._get_from_cache_core(frame, lambda cr: cr, self.check_count_tokens_cache)
-
- async def count_tokens_api(self, prompt: str, is_caching_enabled: bool) -> int:
- """
- Call the count tokens API. This API is free, so we don't ensure resource limits before calling it.
- There are rate limits though: https://docs.anthropic.com/en/docs/build-with-claude/token-counting#pricing-and-rate-limits
- """
-
- self.assert_caching_enabled_if_offline(is_caching_enabled)
-
- frame: FrameType | None = None
- if is_caching_enabled:
- frame = inspect.currentframe()
-
- cache_key: str | None = None
- if is_caching_enabled:
- cache_key, cached_response = await self._get_from_count_tokens_cache(frame)
-
- if cached_response is not None:
- return cached_response.input_tokens
-
- self.assert_not_offline_if_cache_miss(prompt)
-
- non_system_messages, system_messages = _convert_prompt_to_anthropic_messages(prompt)
-
- with _anthropic_exception_manager():
- async with self._get_client() as client:
- raw_response = await client.messages.count_tokens(
- model=self.model_info.model_name,
- messages=non_system_messages,
- system=system_messages,
- )
-
- response = CountTokensResponse(input_tokens=raw_response.input_tokens)
- result = CachedCountTokensResponse(
- response=response,
- inputs=(
- CountTokensInputs(model=self.model_info.model_name, prompt=prompt)
- if self.is_caching_inputs
- else None
- ),
- )
-
- if is_caching_enabled:
- assert cache_key is not None
- async with self.get_count_tokens_response_cache() as cache:
- await cache.set(cache_key, result)
-
- return response.input_tokens
-
- def calculate_cost(
- self,
- prompt_tokens: int,
- completion_tokens: int,
- caching_info: CachingInfo | None = None,
- ) -> float:
- try:
- # find the cost for the prompt, broken down into cache writes and regular input tokens
-
- # if we don't have the caching info, use the basic cost model (we catch the error below)
- if (
- caching_info is None
- or caching_info.provider_specific_data is None
- or self.model_info.provider_specific_info is None
- ):
- raise MissingCachingInfoError(
- f"Missing required info for more precise cost estimates; caching info: {caching_info}, model info: {self.model_info.provider_specific_info}"
- )
- anthropic_caching_usage = caching_info.provider_specific_data
- assert isinstance(anthropic_caching_usage, AnthropicCachingInfo), "Expected AnthropicCachingInfo"
- anthropic_caching_rates = self.model_info.provider_specific_info
- assert isinstance(anthropic_caching_rates, AnthropicModelInfo), "Expected AnthropicModelInfo"
- cache_write_5m_tokens = anthropic_caching_usage.written_5m
- cache_write_1h_tokens = anthropic_caching_usage.written_1h
- cache_read_tokens = caching_info.read_from_cache
- regular_input_tokens = prompt_tokens - cache_write_5m_tokens - cache_write_1h_tokens
-
- input_cost = (
- cache_write_5m_tokens * anthropic_caching_rates.cost_per_5m_cache_write_token
- + cache_write_1h_tokens * anthropic_caching_rates.cost_per_1h_cache_write_token
- + cache_read_tokens * anthropic_caching_rates.cost_per_cache_read_token
- + regular_input_tokens * self.model_info.cost_per_input_token
- )
-
- output_cost = completion_tokens * self.model_info.cost_per_output_token
-
- return input_cost + output_cost
-
- except MissingCachingInfoError as e:
- logger.info("{}; using basic cost model", e)
- return self.basic_calculate_cost(prompt_tokens, completion_tokens)
-
-
-def _get_api_key_or_auth_token() -> tuple[str | None, str | None]:
- api_key = get_secret("ANTHROPIC_API_KEY")
- auth_token = get_secret("ANTHROPIC_AUTH_TOKEN")
- if not api_key and not auth_token:
- raise MissingAPIKeyError("Neither ANTHROPIC_API_KEY nor ANTHROPIC_AUTH_TOKEN environment variable is set")
- return api_key, auth_token
-
-
-_CLAUDE_CODE_SYSTEM_PROMPT = TextBlockParam(
- type="text",
- text="You are Claude Code, Anthropic's official CLI for Claude.",
- cache_control=CacheControlEphemeralParam(type="ephemeral"),
-)
-
-
-def prepend_claude_code_system_prompt(
- system_prompt: str | list[TextBlockParam] | None,
-) -> list[TextBlockParam]:
- """Prepends the system prompt used by Claude Code.
-
- When using the Claude API through Claude Pro/Max subscriptions,
- the Claude API requires this particular system prompt to be set;
- otherwise the request will fail.
-
- For simplicity and consistency,
- we always do this even when it's not strictly required,
- (like when using the Claude API through API keys).
- """
- if not system_prompt:
- return [_CLAUDE_CODE_SYSTEM_PROMPT]
- elif isinstance(system_prompt, str):
- return [
- _CLAUDE_CODE_SYSTEM_PROMPT,
- TextBlockParam(type="text", text=system_prompt),
- ]
- else:
- return [_CLAUDE_CODE_SYSTEM_PROMPT] + system_prompt
diff --git a/imbue_core/imbue_core/agents/llm_apis/anthropic_data_types.py b/imbue_core/imbue_core/agents/llm_apis/anthropic_data_types.py
@@ -1,15 +0,0 @@
-from imbue_core.pydantic_serialization import SerializableModel
-
-
-class AnthropicModelInfo(SerializableModel):
- object_type: str = "AnthropicModelInfo"
- cost_per_5m_cache_write_token: float
- cost_per_1h_cache_write_token: float
- cost_per_cache_read_token: float
-
-
-class AnthropicCachingInfo(SerializableModel):
- object_type: str = "AnthropicCachingInfo"
- # record info on cache writes for 5 minute and 1 hour durations
- written_5m: int
- written_1h: int
diff --git a/imbue_core/imbue_core/agents/llm_apis/api_utils.py b/imbue_core/imbue_core/agents/llm_apis/api_utils.py
@@ -1,110 +0,0 @@
-from typing import Final
-from typing import Iterable
-
-from loguru import logger
-
-from imbue_core.agents.llm_apis.data_types import CachingInfo
-from imbue_core.agents.llm_apis.data_types import ConversationMessage
-from imbue_core.agents.llm_apis.data_types import CostedLanguageModelResponse
-from imbue_core.agents.llm_apis.data_types import LanguageModelResponse
-from imbue_core.agents.llm_apis.data_types import LanguageModelResponseUsage
-from imbue_core.agents.llm_apis.data_types import LanguageModelResponseWithThoughts
-from imbue_core.agents.llm_apis.data_types import ResponseStopReason
-from imbue_core.agents.llm_apis.data_types import ThoughtResponse
-from imbue_core.frozen_utils import FrozenDict
-from imbue_core.frozen_utils import FrozenMapping
-
-_ROLE_TO_OPENAI_ROLE: Final[FrozenMapping] = FrozenDict(
- {
- "HUMAN": "user",
- "ASSISTANT": "assistant",
- "SYSTEM": "system",
- "USER": "user",
- "SYSTEM_CACHED": "system",
- "USER_CACHED": "user",
- }
-)
-
-
-def convert_prompt_to_messages(prompt: str, is_cache_role_preserved: bool = False) -> tuple[ConversationMessage, ...]:
- messages = []
- for raw_message in convert_prompt_to_openai_messages(prompt, is_cache_role_preserved):
- messages.append(ConversationMessage(role=raw_message["role"].upper(), content=raw_message["content"]))
- return tuple(messages)
-
-
-def convert_messages_to_prompt_template(messages: Iterable[ConversationMessage]) -> str:
- return "\n".join(f"[ROLE={message.role.upper()}]\n{message.content}" for message in messages)
-
-
-def create_costed_language_model_response_for_single_result(
- text: str,
- prompt_tokens: int,
- completion_tokens: int,
- stop_reason: ResponseStopReason,
- network_failure_count: int,
- dollars_used: float,
- thoughts: ThoughtResponse | None = None,
- caching_info: CachingInfo | None = None,
-) -> CostedLanguageModelResponse:
- logger.trace("dollars used: {}", dollars_used)
- logger.trace("completion_tokens_used used: {}", completion_tokens)
- if thoughts is None:
- result = LanguageModelResponse(
- text=text,
- token_count=completion_tokens + prompt_tokens,
- stop_reason=stop_reason,
- network_failure_count=network_failure_count,
- )
- else:
- result = LanguageModelResponseWithThoughts(
- text=text,
- token_count=completion_tokens + prompt_tokens,
- stop_reason=stop_reason,
- network_failure_count=network_failure_count,
- thoughts=thoughts,
- )
-
- return CostedLanguageModelResponse(
- usage=LanguageModelResponseUsage(
- prompt_tokens_used=prompt_tokens,
- completion_tokens_used=completion_tokens,
- dollars_used=dollars_used,
- caching_info=caching_info,
- ),
- responses=(result,),
- )
-
-
-# FIXME: we should make sure that all our LLM providers use the same function here, some clean up is required
-def convert_prompt_to_openai_messages(prompt: str, is_cache_role_preserved: bool = False) -> list[dict[str, str]]:
- prompt = prompt.lstrip()
- assert prompt.startswith("[ROLE=")
- prompt = prompt.replace("[ROLE=", "", 1)
- chunks = prompt.split("\n[ROLE=")
- messages: list[dict[str, str]] = []
- for chunk in chunks:
- lines = chunk.split("\n")
- role = lines[0].strip().rstrip("]")
- assert role in (
- "HUMAN",
- "ASSISTANT",
- "USER",
- "SYSTEM",
- "SYSTEM_CACHED",
- "USER_CACHED",
- ), f"Unknown role {role} in prompt {prompt}"
- lines.pop(0)
- if role == "HUMAN":
- role = "USER"
- if len(messages) > 0:
- messages[-1]["content"] = messages[-1]["content"] + "\n"
- content = "\n".join(lines)
- content = content.rstrip()
- fixed_role = _ROLE_TO_OPENAI_ROLE[role]
- if is_cache_role_preserved and role == "SYSTEM_CACHED":
- fixed_role = "SYSTEM_CACHED"
- elif is_cache_role_preserved and role == "USER_CACHED":
- fixed_role = "USER_CACHED"
- messages.append({"role": fixed_role, "content": content})
- return messages
diff --git a/imbue_core/imbue_core/agents/llm_apis/build_apis.py b/imbue_core/imbue_core/agents/llm_apis/build_apis.py
@@ -1,128 +0,0 @@
-from pathlib import Path
-
-from imbue_core.agents.configs import LanguageModelGenerationConfig
-from imbue_core.agents.configs import MockedLanguageModelGenerationConfig
-from imbue_core.agents.configs import OpenAICompatibleModelConfig
-from imbue_core.agents.llm_apis.anthropic_api import AnthropicAPI
-from imbue_core.agents.llm_apis.anthropic_api import AnthropicModelName
-from imbue_core.agents.llm_apis.constants import approximate_token_count
-from imbue_core.agents.llm_apis.gemini_api import GeminiAPI
-from imbue_core.agents.llm_apis.gemini_api import GeminiModelName
-from imbue_core.agents.llm_apis.groq_api import GroqChatAPI
-from imbue_core.agents.llm_apis.groq_api import GroqSupportedModelName
-from imbue_core.agents.llm_apis.language_model_api import LanguageModelAPI
-from imbue_core.agents.llm_apis.mock_api import FileBasedLanguageModelMock
-from imbue_core.agents.llm_apis.mock_api import MockModelName
-from imbue_core.agents.llm_apis.openai_api import OpenAIChatAPI
-from imbue_core.agents.llm_apis.openai_api import OpenAIModelName
-from imbue_core.agents.llm_apis.openai_compatible_api import OpenAICompatibleAPI
-from imbue_core.agents.llm_apis.together_api import TogetherAIModelName
-from imbue_core.agents.llm_apis.together_api import TogetherAPI
-
-
-def build_language_model_from_config(
- config: LanguageModelGenerationConfig,
-) -> LanguageModelAPI:
- if isinstance(config, MockedLanguageModelGenerationConfig):
- return FileBasedLanguageModelMock(cache_path=config.mock_responses_path)
-
- if isinstance(config, OpenAICompatibleModelConfig):
- return OpenAICompatibleAPI(
- model_name=config.model_name,
- base_url=config.custom_base_url,
- api_key_env=config.custom_api_key_env,
- context_window=config.custom_context_window,
- max_output_tokens=config.custom_max_output_tokens,
- cache_path=config.cache_path,
- is_caching_inputs=config.is_caching_inputs,
- is_running_offline=config.is_running_offline,
- is_conversational=True,
- retry_jitter_factor=config.retry_jitter_factor,
- )
-
- if config.model_name in (v for v in MockModelName):
- return FileBasedLanguageModelMock(cache_path=config.cache_path)
- if config.model_name in (v for v in OpenAIModelName):
- return OpenAIChatAPI(
- model_name=config.model_name,
- cache_path=config.cache_path,
- is_caching_inputs=config.is_caching_inputs,
- is_running_offline=config.is_running_offline,
- is_conversational=True,
- is_using_logprobs=config.is_using_logprobs,
- retry_jitter_factor=config.retry_jitter_factor,
- )
- if config.model_name in (v for v in GroqSupportedModelName):
- return GroqChatAPI(
- model_name=config.model_name,
- cache_path=config.cache_path,
- is_caching_inputs=config.is_caching_inputs,
- is_running_offline=config.is_running_offline,
- is_conversational=True,
- is_using_logprobs=config.is_using_logprobs,
- retry_jitter_factor=config.retry_jitter_factor,
- )
- if config.model_name in (v for v in AnthropicModelName):
- return AnthropicAPI(
- model_name=config.model_name,
- cache_path=config.cache_path,
- count_tokens_cache_path=config.count_tokens_cache_path,
- is_caching_inputs=config.is_caching_inputs,
- is_running_offline=config.is_running_offline,
- is_conversational=True,
- is_using_logprobs=config.is_using_logprobs,
- retry_jitter_factor=config.retry_jitter_factor,
- )
- if config.model_name in (v for v in TogetherAIModelName):
- return TogetherAPI(
- model_name=config.model_name,
- cache_path=config.cache_path,
- # count tokens is not supported for Together API
- # count_tokens_cache_path=config.count_tokens_cache_path,
- is_caching_inputs=config.is_caching_inputs,
- is_running_offline=config.is_running_offline,
- is_conversational=True,
- is_using_logprobs=config.is_using_logprobs,
- retry_jitter_factor=config.retry_jitter_factor,
- )
- if config.model_name in (v for v in GeminiModelName):
- return GeminiAPI(
- model_name=config.model_name,
- cache_path=config.cache_path,
- count_tokens_cache_path=config.count_tokens_cache_path,
- is_caching_inputs=config.is_caching_inputs,
- is_running_offline=config.is_running_offline,
- is_conversational=True,
- is_using_logprobs=config.is_using_logprobs,
- retry_jitter_factor=config.retry_jitter_factor,
- )
- # if config.model_name in MISTRAL_CHAT_MODEL_NAMES:
- # return MistralChatAPI(
- # model_name=config.model_name,
- # cache_path=config.cache_path,
- # is_conversational=True,
- # )
-
- raise NotImplementedError(f"{config.model_name} not supported by LanguageModelAPI")
-
-
-def build_language_model_by_name(
- model_name: str,
- cache_path: Path | None = None,
- is_caching_inputs: bool = False,
- is_using_logprobs: bool = False,
-) -> LanguageModelAPI:
- config = LanguageModelGenerationConfig(
- model_name=model_name,
- cache_path=cache_path,
- is_caching_inputs=is_caching_inputs,
- is_using_logprobs=is_using_logprobs,
- )
- return build_language_model_from_config(config)
-
-
-def get_token_count_for_text_and_model(text: str, model_name: str) -> int:
- try:
- return build_language_model_by_name(model_name).count_tokens(text)
- except NotImplementedError:
- return approximate_token_count(text)
diff --git a/imbue_core/imbue_core/agents/llm_apis/common.py b/imbue_core/imbue_core/agents/llm_apis/common.py
@@ -1,73 +0,0 @@
-from imbue_core.agents.llm_apis.anthropic_api import ANTHROPIC_MODEL_INFO_BY_NAME
-from imbue_core.agents.llm_apis.anthropic_api import AnthropicModelName
-from imbue_core.agents.llm_apis.gemini_api import GEMINI_MODEL_INFO_BY_NAME
-from imbue_core.agents.llm_apis.gemini_api import GeminiModelName
-from imbue_core.agents.llm_apis.groq_api import GroqSupportedModelName
-from imbue_core.agents.llm_apis.groq_api import get_model_info as get_groq_model_info
-from imbue_core.agents.llm_apis.mock_api import MY_MOCK_MODEL_INFO
-from imbue_core.agents.llm_apis.models import ModelInfo
-from imbue_core.agents.llm_apis.openai_api import OpenAIModelName
-from imbue_core.agents.llm_apis.openai_api import (
- get_model_info as get_openai_model_info,
-)
-from imbue_core.agents.llm_apis.together_api import TOGETHERAI_MODEL_INFO_BY_NAME
-from imbue_core.agents.llm_apis.together_api import TogetherAIModelName
-
-ModelName = AnthropicModelName | OpenAIModelName | GroqSupportedModelName | TogetherAIModelName | GeminiModelName
-
-
-def get_model_info_from_name(model_name: str) -> ModelInfo:
- if model_name == MY_MOCK_MODEL_INFO.model_name:
- return MY_MOCK_MODEL_INFO
- if model_name in (v for v in AnthropicModelName):
- return ANTHROPIC_MODEL_INFO_BY_NAME[AnthropicModelName(model_name)]
- elif model_name in (v for v in OpenAIModelName):
- return get_openai_model_info(OpenAIModelName(model_name))
- elif model_name in (v for v in GroqSupportedModelName):
- return get_groq_model_info(GroqSupportedModelName(model_name))
- elif model_name in (v for v in TogetherAIModelName):
- return TOGETHERAI_MODEL_INFO_BY_NAME[TogetherAIModelName(model_name)]
- elif model_name in (v for v in GeminiModelName):
- return GEMINI_MODEL_INFO_BY_NAME[GeminiModelName(model_name)]
- else:
- raise Exception(f"Unknown model: {model_name}")
-
-
-def get_model_max_context_length(model_name: str) -> int:
- model_info = get_model_info_from_name(model_name)
- return model_info.max_input_tokens
-
-
-def get_model_max_output_tokens(model_name: str) -> int:
- model_info = get_model_info_from_name(model_name)
- if model_info.max_output_tokens is None:
- raise ValueError(f"Model {model_name} does not have max_output_tokens defined")
- return model_info.max_output_tokens
-
-
-def get_all_model_names() -> list[str]:
- names = []
- names.extend(list(v for v in AnthropicModelName))
- names.extend(list(v for v in OpenAIModelName))
- names.extend(list(v for v in GroqSupportedModelName))
- names.extend(list(v for v in TogetherAIModelName))
- names.extend(list(v for v in GeminiModelName))
- return names
-
-
-def get_formatted_model_name(model_name: str) -> str:
- """Get a nicely formatted model name.
-
- Does things like removing generic prefixes like 'models/' and forward slashes (which can interfere with file names).
-
- Some examples:
-
- - `models/gemini-1.5-flash-001` -> `gemini-1.5-flash-001`
- - 'groq/llama-3.3-70b-versatile' -> 'groq-llama-3.3-70b-versatile'
- - 'claude-3-5-haiku-20241022' -> 'claude-3-5-haiku-20241022'
- - 'together/google/gemma-2-27b-it' -> 'together-google-gemma-2-27b-it'
-
- """
- if model_name.startswith("models/"):
- model_name = model_name[len("models/") :]
- return model_name.replace("/", "-")
diff --git a/imbue_core/imbue_core/agents/llm_apis/data_types.py b/imbue_core/imbue_core/agents/llm_apis/data_types.py
@@ -1,214 +0,0 @@
-import datetime
-import enum
-import math
-from abc import ABC
-from typing import Any
-from typing import Generic
-from typing import TypeVar
-
-import attr
-from pydantic import ValidationInfo
-from pydantic import field_validator
-
-from imbue_core.agents.llm_apis.union_data_types import ProviderSpecificCachingInfoUnion
-from imbue_core.pydantic_serialization import SerializableModel
-from imbue_core.serialization_types import Serializable
-from imbue_core.time_utils import get_current_time
-
-__all__ = [
- "CachedCostedLanguageModelResponse",
- "ConversationMessage",
- "CostedLanguageModelResponse",
- "LanguageModelCompleteInputs",
- "LanguageModelResponse",
- "LanguageModelResponseUsage",
- "LanguageModelResponseWithLogits",
- "LanguageModelResponseWithThoughts",
- "LanguageModelStreamInputs",
- "ModelStr",
- "ResponseStopReason",
- "TokenProbability",
-]
-
-
-class ConversationMessage(SerializableModel):
- role: str
- content: str
-
-
-class ResponseStopReason(enum.StrEnum):
- END_TURN = "end_turn"
- MAX_TOKENS = "max_tokens"
- STOP_SEQUENCE = "stop_sequence"
- ERROR = "error"
- NONE = "none"
- # TODO: We aren't handling any of the below, we should likely error in these cases
- CONTENT_FILTER = "content_filter"
- TOOL_CALLS = "tool_calls"
- FUNCTION_CALL = "function_call"
-
- def response_not_finished(self) -> bool:
- return self in {self.CONTENT_FILTER, self.MAX_TOKENS, self.ERROR}
-
-
-class TokenProbability(SerializableModel):
- token: str
- log_probability: float
- is_stop: bool
-
- @property
- def probability(self) -> float:
- return math.exp(self.log_probability)
-
-
-class ModelResponse(ABC):
- pass
-
-
-class ThoughtResponse(SerializableModel, ModelResponse):
- text: str
- completion_tokens: int
-
-
-@attr.s(auto_attribs=True, frozen=True)
-class LanguageModelResponse(Serializable, ModelResponse):
- text: str
- token_count: int
- stop_reason: ResponseStopReason
- network_failure_count: int
-
- def get_token_probability_sequence(
- self,
- ) -> tuple[tuple[TokenProbability, ...], ...] | None:
- return None
-
-
-@attr.s(auto_attribs=True, frozen=True)
-class LanguageModelResponseWithThoughts(LanguageModelResponse):
- thoughts: ThoughtResponse | None = None
-
-
-@attr.s(auto_attribs=True, frozen=True)
-class LanguageModelResponseWithLogits(LanguageModelResponse):
- # guarantees that the first in each sequence was the one that was selected.
- # the inner sequence are *not* guaranteed to be the same length, nor are they guaranteed to be sorted
- token_probabilities: tuple[tuple[TokenProbability, ...], ...]
-
- def get_token_probability_sequence(
- self,
- ) -> tuple[tuple[TokenProbability, ...], ...] | None:
- return self.token_probabilities
-
-
-@attr.s(auto_attribs=True, frozen=True)
-class CountTokensResponse(Serializable, ModelResponse):
- input_tokens: int
- cached_content_token_count: int | None = None
-
-
-class CachingInfo(SerializableModel):
- read_from_cache: int
-
- # this should contain info that's not the same between providers. e.g. anthropic requires explicit cache writes with 5m or 1h duration,
- # whereas openai does automatic prompt caching at no extra cost; so, we store cache write info here
- provider_specific_data: ProviderSpecificCachingInfoUnion | None = None
-
-
-class LanguageModelResponseUsage(SerializableModel):
- prompt_tokens_used: int
- completion_tokens_used: int
- dollars_used: float
- caching_info: CachingInfo | None = None
-
-
-@attr.s(auto_attribs=True, frozen=True)
-class CostedLanguageModelResponse(Serializable, ModelResponse):
- usage: LanguageModelResponseUsage
- responses: tuple[LanguageModelResponse, ...]
-
-
-class ThinkConfig(SerializableModel):
- # watch out: at least for gemini, this is a soft limit!
- max_tokens: int | None = None
- output_thinking: bool = False
-
-
-class LanguageModelGenerationParams(SerializableModel):
- """Parameters for a single API call to an LLM. Excludes things that you don't want a default for, e.g. the prompt."""
-
- temperature: float = 0.2
- count: int = 1
- max_tokens: int | None = None
- stop: str | None = None
- # specifically to allow generating new responses even when using caching
- seed: int | None = None
- thinking: ThinkConfig | None = None
-
-
-class ModelInputs(SerializableModel, ABC):
- """Base class for inputs to an LLM API call."""
-
-
-class LanguageModelCompleteInputs(ModelInputs):
- """Used to serialize the inputs for an LLM complete call."""
-
- prompt: str
- params: LanguageModelGenerationParams
- network_failure_count: int
-
-
-class LanguageModelStreamInputs(ModelInputs):
- """Used to serialize the inputs for an LLM stream call."""
-
- prompt: str
- params: LanguageModelGenerationParams
-
-
-class CountTokensInputs(ModelInputs):
- """Used to serialize the inputs for a token count call."""
-
- model: str
- prompt: str
-
-
-InputsT = TypeVar("InputsT", bound=ModelInputs)
-ModelResponseT = TypeVar("ModelResponseT", bound=ModelResponse)
-
-
-@attr.s(auto_attribs=True, frozen=True)
-class CachedCostedModelResponse(Serializable, Generic[InputsT, ModelResponseT]):
- response: ModelResponseT | None = None
- error: str | None = None
-
- # The timestamp is used to order cache entries when checking them in unit tests.
- timestamp: datetime.datetime = attr.ib(factory=get_current_time)
-
- # Cache entries are keyed based on an MD5 hash of the inputs to prevent the cache from growing too large.
- # Here, we optionally store the inputs to the request.
- # This is useful for unit tests to highlight changes in the prompt and other parts of the request.
- # But since it can grow very large, we don't always store this information.
- inputs: InputsT | None = None
-
- @field_validator("response", "error")
- def validate_response_or_error(cls, v: Any, info: ValidationInfo) -> Any:
- if "response" in info.data and "error" in info.data:
- if not ((info.data["response"] is None) ^ (info.data["error"] is None)):
- raise ValueError("Must provide exactly one of response or error")
- return v
-
-
-class CachedCostedLanguageModelResponse(
- CachedCostedModelResponse[
- LanguageModelCompleteInputs | LanguageModelStreamInputs,
- CostedLanguageModelResponse,
- ]
-):
- pass
-
-
-class CachedCountTokensResponse(CachedCostedModelResponse[CountTokensInputs, CountTokensResponse]):
- pass
-
-
-# to allow type checking to work when model names are passed as strings
-ModelStr = str
diff --git a/imbue_core/imbue_core/agents/llm_apis/gemini_api.py b/imbue_core/imbue_core/agents/llm_apis/gemini_api.py
@@ -1,526 +0,0 @@
-import enum
-import inspect
-from contextlib import contextmanager
-from pathlib import Path
-from types import FrameType
-from typing import AsyncGenerator
-from typing import Callable
-from typing import Final
-from typing import Iterable
-from typing import Iterator
-from typing import TypeVar
-
-import google.genai as genai
-import httpx
-from google.genai.errors import APIError
-from google.genai.types import BlockedReason
-from google.genai.types import ContentListUnion
-from google.genai.types import ContentUnion
-from google.genai.types import FinishReason
-from google.genai.types import GenerateContentConfig
-from google.genai.types import GenerateContentResponse
-from google.genai.types import HarmProbability
-from google.genai.types import ModelContent
-from google.genai.types import Part
-from google.genai.types import ThinkingConfig
-from google.genai.types import UserContent
-from loguru import logger
-from pydantic.functional_validators import field_validator
-
-from imbue_core.agents.llm_apis.api_utils import convert_prompt_to_messages
-from imbue_core.agents.llm_apis.api_utils import (
- create_costed_language_model_response_for_single_result,
-)
-from imbue_core.agents.llm_apis.data_types import CachedCountTokensResponse
-from imbue_core.agents.llm_apis.data_types import CostedLanguageModelResponse
-from imbue_core.agents.llm_apis.data_types import CountTokensInputs
-from imbue_core.agents.llm_apis.data_types import CountTokensResponse
-from imbue_core.agents.llm_apis.data_types import LanguageModelGenerationParams
-from imbue_core.agents.llm_apis.data_types import ResponseStopReason
-from imbue_core.agents.llm_apis.data_types import ThoughtResponse
-from imbue_core.agents.llm_apis.errors import BadAPIRequestError
-from imbue_core.agents.llm_apis.errors import LanguageModelInvalidModelNameError
-from imbue_core.agents.llm_apis.errors import MissingAPIKeyError
-from imbue_core.agents.llm_apis.errors import TransientLanguageModelError
-from imbue_core.agents.llm_apis.errors import UnsetCachePathError
-from imbue_core.agents.llm_apis.language_model_api import LanguageModelAPI
-from imbue_core.agents.llm_apis.models import ModelInfo
-from imbue_core.agents.llm_apis.stream import LanguageModelStreamEvent
-from imbue_core.async_monkey_patches import log_exception
-from imbue_core.caching import AsyncCache
-from imbue_core.frozen_utils import FrozenDict
-from imbue_core.frozen_utils import FrozenMapping
-from imbue_core.itertools import only
-from imbue_core.secrets_utils import get_secret
-
-
-class GeminiModelName(enum.StrEnum):
- GEMINI_1_0_PRO = "models/gemini-1.0-pro-001"
- GEMINI_1_5_FLASH = "models/gemini-1.5-flash-001"
- GEMINI_1_5_PRO = "models/gemini-1.5-pro-001"
- GEMINI_1_5_PRO_2 = "models/gemini-1.5-pro-002"
- GEMINI_1_5_FLASH_2 = "models/gemini-1.5-flash-002"
- GEMINI_2_0_FLASH = "models/gemini-2.0-flash-001"
- GEMINI_2_5_FLASH = "models/gemini-2.5-flash"
- GEMINI_2_5_FLASH_LITE_PREVIEW = "models/gemini-2.5-flash-lite-preview-06-17"
-
-
-# Rate limits for Google Gemini models based on published API documentation
-# Reference: https://ai.google.dev/gemini-api/docs/rate-limits#tier-3
-# Using Tier 3 rate limits
-
-GEMINI_MODEL_INFO_BY_NAME: FrozenMapping[GeminiModelName, ModelInfo] = FrozenDict(
- {
- # https://ai.google.dev/gemini-api/docs/models/gemini
- # https://ai.google.dev/pricing
- # For pricing there are different rates depending on context/prompt size, so below we use the most
- # expensive value. Note that this only kicks in at 128k tokens, the cost for most prompts is 2x lower
- GeminiModelName.GEMINI_1_0_PRO: ModelInfo(
- model_name="models/gemini-1.0-pro-001",
- cost_per_input_token=0.5 / 1_000_000,
- cost_per_output_token=1.5 / 1_000_000,
- max_input_tokens=30_720,
- max_output_tokens=2048,
- rate_limit_req=2000 / 60, # 2000 RPM = 33.33 RPS
- ),
- GeminiModelName.GEMINI_1_5_FLASH: ModelInfo(
- model_name="models/gemini-1.5-flash-001",
- cost_per_input_token=0.15 / 1_000_000,
- cost_per_output_token=0.60 / 1_000_000,
- max_input_tokens=1_048_576,
- max_output_tokens=8192,
- rate_limit_req=30000 / 60, # 30000 RPM = 500.00 RPS
- ),
- GeminiModelName.GEMINI_1_5_FLASH_2: ModelInfo(
- model_name="models/gemini-1.5-flash-002",
- cost_per_input_token=0.15 / 1_000_000,
- cost_per_output_token=0.60 / 1_000_000,
- max_input_tokens=1_048_576,
- max_output_tokens=8192,
- rate_limit_req=30000 / 60, # 30000 RPM = 500.00 RPS
- ),
- GeminiModelName.GEMINI_1_5_PRO: ModelInfo(
- model_name="models/gemini-1.5-pro-001",
- cost_per_input_token=2.5 / 1_000_000,
- cost_per_output_token=10.0 / 1_000_000,
- max_input_tokens=2_097_152,
- max_output_tokens=8192,
- rate_limit_req=4000 / 60, # 4000 RPM = 66.67 RPS
- ),
- GeminiModelName.GEMINI_1_5_PRO_2: ModelInfo(
- model_name="models/gemini-1.5-pro-002",
- cost_per_input_token=2.5 / 1_000_000,
- cost_per_output_token=10.0 / 1_000_000,
- max_input_tokens=2_097_152,
- max_output_tokens=8192,
- rate_limit_req=4000 / 60, # 4000 RPM = 66.67 RPS
- ),
- GeminiModelName.GEMINI_2_0_FLASH: ModelInfo(
- model_name="models/gemini-2.0-flash-001",
- cost_per_input_token=0.1 / 1_000_000,
- cost_per_output_token=0.4 / 1_000_000,
- max_input_tokens=1_048_576,
- max_output_tokens=8192,
- rate_limit_req=30000 / 60, # 30000 RPM = 500.00 RPS
- ),
- GeminiModelName.GEMINI_2_5_FLASH: ModelInfo(
- model_name="models/gemini-2.5-flash",
- cost_per_input_token=0.3 / 1_000_000,
- cost_per_output_token=2.5 / 1_000_000,
- max_input_tokens=1_048_576,
- max_output_tokens=65536,
- rate_limit_req=10_000 / 60, # 10000 RPM = 166.67 RPS
- rate_limit_tok=8_000_000 / 60, # 8,000,000 TPM = 133,333.33 TPS
- max_thinking_budget=24576,
- ),
- GeminiModelName.GEMINI_2_5_FLASH_LITE_PREVIEW: ModelInfo(
- model_name="models/gemini-2.5-flash-lite-preview-06-17",
- cost_per_input_token=0.1 / 1_000_000,
- cost_per_output_token=0.4 / 1_000_000,
- max_input_tokens=1_000_000,
- max_output_tokens=64_000,
- # these are the tier 2 rate limits. the above claims that we're on tier 3, but i've never actually seen that
- rate_limit_req=10_000 / 60,
- rate_limit_tok=10_000_000 / 60,
- # rate_limit_req=30_000 / 60, # 30000 RPM = 500.00 RPS
- # rate_limit_tok=30_000_000 / 60, # 30,000,000 TPM = 500,000 TPS
- max_thinking_budget=24_576,
- ),
- }
-)
-
-
-_ROLE_TO_GEMINI_ROLE: Final[FrozenMapping[str, str]] = FrozenDict(
- {
- "HUMAN": "user",
- "ASSISTANT": "model",
- "USER": "user",
- "SYSTEM": "user",
- }
-)
-
-NO_SIMPLE_TEXT_ERROR = "".join(
- [
- "The `response.text` quick accessor only works for ",
- "simple (single-`Part`) text responses. This response is not simple text.",
- "Use the `result.parts` accessor or the full ",
- "`result.candidates[index].content.parts` lookup ",
- "instead.",
- ]
-)
-
-_GEMINI_STOP_REASON_TO_STOP_REASON: Final[FrozenMapping[FinishReason, ResponseStopReason]] = FrozenDict(
- {
- # Gemini treats stop due to natural stop point and provided stop sequence the same
- FinishReason.STOP: ResponseStopReason.END_TURN,
- FinishReason.MAX_TOKENS: ResponseStopReason.MAX_TOKENS,
- FinishReason.SAFETY: ResponseStopReason.CONTENT_FILTER,
- # Recitation means the content was flagged for being memorized, i.e. the LLM just
- # copied data from the training data (@johnny at least that's how I understood the docs)
- # https://ai.google.dev/api/generate-content#FinishReason
- FinishReason.RECITATION: ResponseStopReason.CONTENT_FILTER,
- FinishReason.OTHER: ResponseStopReason.NONE,
- FinishReason.FINISH_REASON_UNSPECIFIED: ResponseStopReason.NONE,
- }
-)
-
-T = TypeVar("T")
-
-
-def only_and_not_none(iterable: Iterable[T] | None) -> T:
- in_value = iterable if iterable is not None else []
- return only(in_value)
-
-
-def _is_flagged_as_unsafe(api_result: GenerateContentResponse) -> bool:
- if api_result.prompt_feedback is None:
- return False
- block_reason = api_result.prompt_feedback.block_reason
- if block_reason == BlockedReason.SAFETY:
- return True
- candidate = only_and_not_none(api_result.candidates)
- if candidate.finish_reason == FinishReason.SAFETY:
- return True
- if candidate.finish_reason == FinishReason.OTHER and any(
- rating.probability != HarmProbability.NEGLIGIBLE for rating in (candidate.safety_ratings or [])
- ):
- return True
- return False
-
-
-def _is_flagged_as_recitation(api_result: GenerateContentResponse) -> bool:
- candidate = only_and_not_none(api_result.candidates)
- finish_reason = candidate.finish_reason
- if finish_reason == FinishReason.RECITATION:
- return True
- return False
-
-
-def role_to_content(role: str, parts: list[Part]) -> ContentUnion:
- match role:
- case "user":
- return UserContent(parts=parts)
- case "model":
- return ModelContent(parts=parts)
- case _:
- raise BadAPIRequestError(f"Invalid role: {role}")
-
-
-def convert_prompt_to_gemini_messages(prompt: str) -> ContentListUnion:
- messages: list[ContentUnion] = []
- parts = []
- last_role = None
- for message in convert_prompt_to_messages(prompt):
- role = _ROLE_TO_GEMINI_ROLE[message.role]
- parts.append(Part(text=f"\n{message.content}"))
- if last_role != role and last_role is not None:
- messages.append(role_to_content(last_role, parts))
- parts = []
- last_role = role
- if len(parts) > 0:
- assert last_role is not None
- messages.append(role_to_content(last_role, parts))
- return messages
-
-
-@contextmanager
-def _gemini_exception_manager() -> Iterator[None]:
- """Simple context manager for parsing gemini API exceptions."""
- # TODO probably some exceptions missing here. The google.ai docs/code is annoying to parse
- try:
- yield
- except AssertionError as e:
- logger.info("The Gemini prompt is invalid.")
- raise BadAPIRequestError(str(e)) from e
- except APIError as e:
- logger.info("Gemini failed to generate content.")
- raise BadAPIRequestError(str(e)) from e
- except ValueError as e:
- logger.info("Gemini did not return a simple text response.")
- raise BadAPIRequestError(str(e)) from e
- except AttributeError as e:
- logger.info("There is an error with the Gemini prompt or processing code: {}.", str(e))
- raise BadAPIRequestError(str(e)) from e
- except httpx.RemoteProtocolError as e:
- logger.info(str(e))
- raise TransientLanguageModelError("httpx.RemoteProtocolError") from e
- except (BadAPIRequestError, TransientLanguageModelError, MissingAPIKeyError):
- # we already raised this error ourselves earlier, so we don't need to mark it as unknown
- raise
- except Exception as e:
- # we catch TransientLanguageModelError later to retry it, but we still want to log it so it's not silent
- log_exception(
- e,
- "Failed to generate output from Gemini, unknown error of type {type_name}",
- type_name=type(e).__name__,
- )
- raise TransientLanguageModelError("Unknown error") from e
-
-
-R = TypeVar("R")
-
-
-def fmap(fn: Callable[[T], R], values: T | None) -> R | None:
- if values is None:
- return None
- return fn(values)
-
-
-class GeminiAPI(LanguageModelAPI):
- model_name: GeminiModelName = GeminiModelName.GEMINI_1_5_FLASH
- is_conversational: bool = True
-
- count_tokens_cache_path: Path | None = None
-
- @field_validator("model_name") # pyre-ignore[56]: pyre doesn't understand pydantic
- @classmethod
- def validate_model_name(cls, v: str) -> str:
- if v not in GEMINI_MODEL_INFO_BY_NAME:
- raise LanguageModelInvalidModelNameError(v, cls.__name__, list(GEMINI_MODEL_INFO_BY_NAME))
- return v
-
- @property
- def model_info(self) -> ModelInfo:
- return GEMINI_MODEL_INFO_BY_NAME[self.model_name]
-
- def _get_client(self) -> genai.Client:
- api_key = get_secret("GOOGLE_API_KEY")
- if not api_key:
- raise MissingAPIKeyError("GOOGLE_API_KEY environment variable is not set")
- return genai.Client(api_key=api_key)
-
- async def _call_api(
- self,
- prompt: str,
- params: LanguageModelGenerationParams,
- network_failure_count: int = 0,
- ) -> CostedLanguageModelResponse:
- # TODO: check if this is still true
- assert params.count == 1, "Gemini only supports a single completion"
- messages = convert_prompt_to_gemini_messages(prompt)
- with _gemini_exception_manager():
- client = self._get_client()
- generation_config = GenerateContentConfig(
- temperature=params.temperature,
- candidate_count=params.count,
- stop_sequences=fmap(lambda x: [x], params.stop),
- max_output_tokens=params.max_tokens,
- thinking_config=fmap(
- lambda thinking: ThinkingConfig(
- thinking_budget=thinking.max_tokens,
- include_thoughts=thinking.output_thinking,
- ),
- params.thinking,
- ),
- )
-
- api_result: GenerateContentResponse = await client.aio.models.generate_content(
- model=self.model_info.model_name,
- contents=messages,
- config=generation_config,
- )
-
- prompt_tokens = self.count_tokens(prompt)
-
- if (
- api_result.prompt_feedback is not None
- and api_result.prompt_feedback.block_reason is not None
- and api_result.prompt_feedback.block_reason != BlockedReason.BLOCKED_REASON_UNSPECIFIED
- ):
- logger.info(
- f"Gemini blocked output: {messages=}, {api_result.prompt_feedback.block_reason=}, {api_result.prompt_feedback.safety_ratings=}"
- )
- return create_costed_language_model_response_for_single_result(
- text="",
- prompt_tokens=prompt_tokens,
- completion_tokens=0,
- stop_reason=ResponseStopReason.NONE,
- network_failure_count=network_failure_count,
- dollars_used=self.calculate_cost(prompt_tokens, 0), # guestimate of cost,
- )
-
- if _is_flagged_as_unsafe(api_result) or _is_flagged_as_recitation(api_result):
- block_reason = fmap(lambda x: x.block_reason, api_result.prompt_feedback)
- safety_ratings = (
- api_result.prompt_feedback.safety_ratings if api_result.prompt_feedback is not None else None
- )
- logger.info(
- "Gemini flagged output: block_reason={block_reason}, safety_ratings={safety_ratings}",
- block_reason=block_reason,
- safety_ratings=safety_ratings,
- )
- return create_costed_language_model_response_for_single_result(
- text="",
- prompt_tokens=prompt_tokens,
- completion_tokens=0,
- stop_reason=ResponseStopReason.CONTENT_FILTER,
- network_failure_count=network_failure_count,
- dollars_used=self.calculate_cost(prompt_tokens, 0),
- )
-
- candidate = only_and_not_none(api_result.candidates)
- finish_reason = candidate.finish_reason
- parsed_finish_reason = (
- _GEMINI_STOP_REASON_TO_STOP_REASON[finish_reason]
- if finish_reason is not None
- else ResponseStopReason.NONE
- )
-
- if finish_reason not in [FinishReason.MAX_TOKENS, FinishReason.STOP]:
- block_reason = fmap(lambda x: x.block_reason, api_result.prompt_feedback)
- safety_ratings = fmap(lambda x: x.safety_ratings, api_result.prompt_feedback)
- logger.info(
- f"Gemini did not return a simple text response, {block_reason=}, {safety_ratings=}, {finish_reason=}, {candidate.safety_ratings=}"
- )
- return create_costed_language_model_response_for_single_result(
- text="",
- prompt_tokens=prompt_tokens,
- completion_tokens=0,
- stop_reason=parsed_finish_reason,
- network_failure_count=network_failure_count,
- dollars_used=self.calculate_cost(prompt_tokens, 0),
- )
-
- text = api_result.text
-
- thoughts_list = fmap(
- lambda content: fmap(
- lambda parts: [part.text for part in parts if part.thought],
- content.parts,
- ),
- candidate.content,
- )
- if not thoughts_list:
- thoughts = None
- else:
- thoughts = only(thoughts_list)
-
- if text is None:
- if finish_reason == FinishReason.MAX_TOKENS and generation_config.thinking_config is not None:
- raise BadAPIRequestError(
- "Gemini ran out of tokens while thinking and did not return a text response"
- )
- logger.info("Non-simple-text response: {}", api_result)
- raise BadAPIRequestError("Gemini did not return a simple text response (text is None)")
-
- prompt_tokens = (
- api_result.usage_metadata.prompt_token_count
- if api_result.usage_metadata is not None and api_result.usage_metadata.prompt_token_count is not None
- else self.count_tokens(prompt)
- )
-
- thought_tokens = (
- api_result.usage_metadata.thoughts_token_count
- if api_result.usage_metadata is not None and api_result.usage_metadata.thoughts_token_count is not None
- else 0
- )
-
- output_tokens = (
- api_result.usage_metadata.candidates_token_count
- if api_result.usage_metadata is not None
- and api_result.usage_metadata.candidates_token_count is not None
- else self.count_tokens(text)
- )
-
- completion_tokens = output_tokens + thought_tokens
-
- dollars_used = self.calculate_cost(prompt_tokens, completion_tokens)
- logger.trace(text)
- logger.trace("Dollars used: {}", dollars_used)
- return create_costed_language_model_response_for_single_result(
- text=text,
- prompt_tokens=prompt_tokens,
- completion_tokens=completion_tokens,
- stop_reason=parsed_finish_reason,
- network_failure_count=network_failure_count,
- dollars_used=dollars_used,
- thoughts=fmap(
- lambda x: ThoughtResponse(text=x, completion_tokens=thought_tokens),
- thoughts,
- ),
- )
-
- def _get_api_stream(
- self,
- prompt: str,
- params: LanguageModelGenerationParams,
- ) -> AsyncGenerator[LanguageModelStreamEvent, None]:
- # TODO Implement streaming support (?)
- raise NotImplementedError()
-
- # TODO: these are the same as in anthropic_api.py. it might be good to refactor so that both AnthropicAPI and GeminiAPI inherit from a class LanguageModelAPIWithCountTokens which has these methods
- def get_count_tokens_response_cache(self) -> AsyncCache[CachedCountTokensResponse]:
- if self.count_tokens_cache_path is None:
- raise UnsetCachePathError()
- return AsyncCache(self.count_tokens_cache_path, CachedCountTokensResponse)
-
- async def check_count_tokens_cache(self, cache_key: str) -> CountTokensResponse | None:
- return await self.check_cache_core(self.get_count_tokens_response_cache, cache_key)
-
- async def _get_from_count_tokens_cache(
- self, frame: FrameType | None
- ) -> tuple[str | None, CountTokensResponse | None]:
- return await self._get_from_cache_core(frame, lambda cr: cr, self.check_count_tokens_cache)
-
- async def count_tokens_api(self, text: str, is_caching_enabled: bool) -> int | None:
- """Call the count_tokens api to get a definitive token count. May be fragile and is definitely slow."""
-
- self.assert_caching_enabled_if_offline(is_caching_enabled)
-
- frame: FrameType | None = None
- if is_caching_enabled:
- frame = inspect.currentframe()
-
- cache_key: str | None = None
- if is_caching_enabled:
- cache_key, cached_response = await self._get_from_count_tokens_cache(frame)
-
- if cached_response is not None:
- return cached_response.input_tokens
-
- self.assert_not_offline_if_cache_miss(text)
-
- with _gemini_exception_manager():
- client = self._get_client()
- response = client.models.count_tokens(model=self.model_info.model_name, contents=text)
-
- total_tokens = response.total_tokens
- if total_tokens is None:
- raise TransientLanguageModelError("Gemini did not return a valid token count")
-
- result = CachedCountTokensResponse(
- response=CountTokensResponse(
- input_tokens=total_tokens,
- cached_content_token_count=response.cached_content_token_count,
- ),
- inputs=(
- CountTokensInputs(model=self.model_info.model_name, prompt=text) if self.is_caching_inputs else None
- ),
- )
-
- if is_caching_enabled:
- assert cache_key is not None
- async with self.get_count_tokens_response_cache() as cache:
- await cache.set(cache_key, result)
-
- return total_tokens
diff --git a/imbue_core/imbue_core/agents/llm_apis/groq_api.py b/imbue_core/imbue_core/agents/llm_apis/groq_api.py
@@ -1,357 +0,0 @@
-import asyncio
-import enum
-import math
-from contextlib import contextmanager
-from typing import AsyncGenerator
-from typing import Final
-from typing import Iterator
-from typing import Mapping
-
-import httpx
-from groq import APIConnectionError
-from groq import APIError
-from groq import AsyncGroq
-from groq import AsyncStream
-from groq import BadRequestError
-from groq import RateLimitError
-from groq.types.chat import ChatCompletion
-from loguru import logger
-from pydantic.functional_validators import field_validator
-
-from imbue_core.agents.llm_apis.api_utils import convert_prompt_to_openai_messages
-from imbue_core.agents.llm_apis.data_types import CostedLanguageModelResponse
-from imbue_core.agents.llm_apis.data_types import LanguageModelGenerationParams
-from imbue_core.agents.llm_apis.data_types import LanguageModelResponse
-from imbue_core.agents.llm_apis.data_types import LanguageModelResponseUsage
-from imbue_core.agents.llm_apis.data_types import ResponseStopReason
-from imbue_core.agents.llm_apis.errors import BadAPIRequestError
-from imbue_core.agents.llm_apis.errors import LanguageModelInvalidModelNameError
-from imbue_core.agents.llm_apis.errors import MissingAPIKeyError
-from imbue_core.agents.llm_apis.errors import PromptTooLongError
-from imbue_core.agents.llm_apis.errors import TransientLanguageModelError
-from imbue_core.agents.llm_apis.language_model_api import LanguageModelAPI
-from imbue_core.agents.llm_apis.models import ModelInfo
-from imbue_core.agents.llm_apis.stream import LanguageModelStreamDeltaEvent
-from imbue_core.agents.llm_apis.stream import LanguageModelStreamEndEvent
-from imbue_core.agents.llm_apis.stream import LanguageModelStreamEvent
-from imbue_core.agents.llm_apis.stream import LanguageModelStreamStartEvent
-from imbue_core.frozen_utils import FrozenDict
-from imbue_core.frozen_utils import FrozenMapping
-from imbue_core.itertools import only
-from imbue_core.secrets_utils import get_secret
-
-# note: we require that these model versions are explicit, just like the rest of our dependencies
-# the reason is that these models are actually now mostly deterministic, and it is much easier to debug if we know what model was used
-# also, there's no need to troll yourself by wondering why results have improved (or gotten worse) when you dont realized that the version has shifted under you
-# if you want to use an upgraded model, just upgrade the model to the key displayed on the website
-# please do NOT set these back to the generic model names!
-
-
-# TODO: there are likely more models to add
-class GroqSupportedModelName(enum.StrEnum):
- GROQ_GEMMA2_9B_IT = "groq/gemma2-9b-it"
- GROQ_LLAMA3_70B_8192 = "groq/llama3-70b-8192"
- GROQ_LLAMA3_8B_8192 = "groq/llama3-8b-8192"
- GROQ_LLAMA_3_3_70B_SPECDEC = "groq/llama-3.3-70b-specdec"
- GROQ_MIXTRAL_8X7B_32768 = "groq/mixtral-8x7b-32768"
- GROQ_LLAMA_3_3_70B_VERSATILE = "groq/llama-3.3-70b-versatile"
- GROQ_LLAMA_3_1_8B_INSTANT = "groq/llama-3.1-8b-instant"
- GROQ_LLAMA_3_2_1B_PREVIEW = "groq/llama-3.2-1b-preview"
- GROQ_LLAMA_3_2_3B_PREVIEW = "groq/llama-3.2-3b-preview"
-
-
-# Rate limits for Groq models based on custom rate limits for our organization.
-# See here https://console.groq.com/dashboard/limits (requires login, use your Google account)
-
-GROQ_MODEL_INFO_BY_NAME: FrozenMapping[GroqSupportedModelName, ModelInfo] = FrozenDict(
- {
- GroqSupportedModelName.GROQ_GEMMA2_9B_IT: ModelInfo(
- model_name=str(GroqSupportedModelName.GROQ_GEMMA2_9B_IT),
- cost_per_input_token=0.20 / 1_000_000,
- cost_per_output_token=0.20 / 1_000_000,
- max_input_tokens=8192,
- max_output_tokens=None,
- rate_limit_req=30 / 60, # 30 RPM = 0.50 RPS
- ),
- GroqSupportedModelName.GROQ_LLAMA3_70B_8192: ModelInfo(
- model_name=str(GroqSupportedModelName.GROQ_LLAMA3_70B_8192),
- cost_per_input_token=0.59 / 1_000_000,
- cost_per_output_token=0.79 / 1_000_000,
- max_input_tokens=8192,
- max_output_tokens=None,
- rate_limit_req=30 / 60, # 30 RPM = 0.50 RPS
- ),
- GroqSupportedModelName.GROQ_LLAMA3_8B_8192: ModelInfo(
- model_name=str(GroqSupportedModelName.GROQ_LLAMA3_8B_8192),
- cost_per_input_token=0.05 / 1_000_000,
- cost_per_output_token=0.08 / 1_000_000,
- max_input_tokens=8192,
- max_output_tokens=None,
- rate_limit_req=30 / 60, # 30 RPM = 0.50 RPS
- ),
- GroqSupportedModelName.GROQ_LLAMA_3_3_70B_SPECDEC: ModelInfo(
- model_name=str(GroqSupportedModelName.GROQ_LLAMA_3_3_70B_SPECDEC),
- cost_per_input_token=0.59 / 1_000_000,
- cost_per_output_token=0.99 / 1_000_000,
- max_input_tokens=8192,
- max_output_tokens=None,
- rate_limit_req=30 / 60, # 30 RPM = 0.50 RPS
- ),
- GroqSupportedModelName.GROQ_MIXTRAL_8X7B_32768: ModelInfo(
- model_name=str(GroqSupportedModelName.GROQ_MIXTRAL_8X7B_32768),
- cost_per_input_token=0.24 / 1_000_000,
- cost_per_output_token=0.24 / 1_000_000,
- max_input_tokens=32768,
- max_output_tokens=None,
- rate_limit_req=30 / 60, # 30 RPM = 0.50 RPS
- ),
- GroqSupportedModelName.GROQ_LLAMA_3_3_70B_VERSATILE: ModelInfo(
- model_name=str(GroqSupportedModelName.GROQ_LLAMA_3_3_70B_VERSATILE),
- cost_per_input_token=0.59 / 1_000_000,
- cost_per_output_token=0.79 / 1_000_000,
- max_input_tokens=128_000,
- max_output_tokens=None,
- rate_limit_req=30 / 60, # 30 RPM = 0.50 RPS
- ),
- GroqSupportedModelName.GROQ_LLAMA_3_1_8B_INSTANT: ModelInfo(
- model_name=str(GroqSupportedModelName.GROQ_LLAMA_3_1_8B_INSTANT),
- cost_per_input_token=0.05 / 1_000_000,
- cost_per_output_token=0.08 / 1_000_000,
- max_input_tokens=128_000,
- max_output_tokens=None,
- rate_limit_req=30 / 60, # 30 RPM = 0.50 RPS
- ),
- GroqSupportedModelName.GROQ_LLAMA_3_2_1B_PREVIEW: ModelInfo(
- model_name=str(GroqSupportedModelName.GROQ_LLAMA_3_2_1B_PREVIEW),
- cost_per_input_token=0.04 / 1_000_000,
- cost_per_output_token=0.04 / 1_000_000,
- max_input_tokens=128_000,
- max_output_tokens=None,
- rate_limit_req=30 / 60, # 30 RPM = 0.50 RPS
- ),
- GroqSupportedModelName.GROQ_LLAMA_3_2_3B_PREVIEW: ModelInfo(
- model_name=str(GroqSupportedModelName.GROQ_LLAMA_3_2_3B_PREVIEW),
- cost_per_input_token=0.06 / 1_000_000,
- cost_per_output_token=0.06 / 1_000_000,
- max_input_tokens=128_000,
- max_output_tokens=None,
- rate_limit_req=30 / 60, # 30 RPM = 0.50 RPS
- ),
- }
-)
-
-
-def get_model_info(model_name: GroqSupportedModelName) -> ModelInfo:
- return GROQ_MODEL_INFO_BY_NAME[model_name]
-
-
-_CAPACITY_SEMAPHOR_BY_MODEL_NAME: Mapping[str, asyncio.Semaphore] = {
- GroqSupportedModelName.GROQ_GEMMA2_9B_IT: asyncio.Semaphore(100),
- GroqSupportedModelName.GROQ_LLAMA3_70B_8192: asyncio.Semaphore(100),
- GroqSupportedModelName.GROQ_LLAMA3_8B_8192: asyncio.Semaphore(100),
- GroqSupportedModelName.GROQ_LLAMA_3_3_70B_SPECDEC: asyncio.Semaphore(100),
- GroqSupportedModelName.GROQ_MIXTRAL_8X7B_32768: asyncio.Semaphore(100),
- GroqSupportedModelName.GROQ_LLAMA_3_3_70B_VERSATILE: asyncio.Semaphore(100),
- GroqSupportedModelName.GROQ_LLAMA_3_1_8B_INSTANT: asyncio.Semaphore(100),
- GroqSupportedModelName.GROQ_LLAMA_3_2_1B_PREVIEW: asyncio.Semaphore(100),
- GroqSupportedModelName.GROQ_LLAMA_3_2_3B_PREVIEW: asyncio.Semaphore(100),
-}
-
-
-def _get_capacity_semaphor(model_name: str) -> asyncio.Semaphore:
- return _CAPACITY_SEMAPHOR_BY_MODEL_NAME[model_name]
-
-
-# ref: https://github.com/groq/groq-python/blob/b74ce9e301115520c744e18425653a4c783cb6f5/src/groq/types/chat/chat_completion_chunk.py#L86
-_GROQ_STOP_REASON_TO_STOP_REASON: Final[FrozenMapping[str, ResponseStopReason]] = FrozenDict(
- {
- # Groq copies OpenAI and treats stop due to natural stop point and provided stop sequence the same
- "stop": ResponseStopReason.END_TURN,
- "length": ResponseStopReason.MAX_TOKENS,
- "tool_calls": ResponseStopReason.TOOL_CALLS,
- "function_call": ResponseStopReason.FUNCTION_CALL,
- "content_filter": ResponseStopReason.CONTENT_FILTER,
- }
-)
-
-
-@contextmanager
-def _groq_exception_manager() -> Iterator[None]:
- """Simple context manager for parsing groq exceptions mostly based on how we parse OpenAI API exceptions."""
- try:
- yield
- except BadRequestError as e:
- logger.info("BadAPIRequestError {}", e)
- raise BadAPIRequestError(str(e)) from e
- except APIConnectionError as e:
- logger.info("Rate limited? Received APIConnectionError {}", e)
- raise TransientLanguageModelError("APIConnectionError") from e
- except RateLimitError as e:
- logger.info("Rate limited? {}", e)
- raise TransientLanguageModelError("RateLimitError") from e
- except httpx.RemoteProtocolError as e:
- logger.info("{}", e)
- raise TransientLanguageModelError("httpx.RemoteProtocolError") from e
- except APIError as e:
- if e.body["code"] == "context_length_exceeded": # type: ignore
- # TODO: eventually fix elsewhere, since this doesn't actually give you any information in the body...
- raise PromptTooLongError(prompt_len=1, max_prompt_len=1)
- raise TransientLanguageModelError("APIError") from e
-
-
-class GroqChatAPI(LanguageModelAPI):
- model_name: GroqSupportedModelName = GroqSupportedModelName.GROQ_LLAMA3_8B_8192
- is_conversational: bool = True
- presence_penalty: float = 0.0
- # this shouldn't really ever even be used, but just in case
- stop_token_log_probability: float = math.log(0.9999)
-
- @field_validator("model_name") # pyre-ignore[56]: pyre doesn't understand pydantic
- @classmethod
- def validate_model_name(cls, v: str) -> str:
- if v not in GROQ_MODEL_INFO_BY_NAME:
- raise LanguageModelInvalidModelNameError(v, cls.__name__, list(GROQ_MODEL_INFO_BY_NAME))
- return v
-
- @property
- def model_info(self) -> ModelInfo:
- return GROQ_MODEL_INFO_BY_NAME[self.model_name]
-
- @property
- def external_model_name(self) -> str:
- return self.model_name.replace("groq/", "")
-
- def _get_client(self) -> AsyncGroq:
- api_key = get_secret("GROQ_API_KEY")
- if not api_key:
- raise MissingAPIKeyError("GROQ_API_KEY environment variable is not set")
- return AsyncGroq(api_key=api_key)
-
- async def _call_api(
- self,
- prompt: str,
- params: LanguageModelGenerationParams,
- network_failure_count: int = 0,
- ) -> CostedLanguageModelResponse:
- with _groq_exception_manager():
- messages = convert_prompt_to_openai_messages(prompt)
- client = self._get_client()
- async with _get_capacity_semaphor(self.model_name):
- # logger.info("Open requests: {}", semaphor._value)
- api_result = await client.chat.completions.create(
- model=self.external_model_name,
- messages=messages, # type: ignore
- max_tokens=params.max_tokens,
- n=params.count,
- temperature=params.temperature,
- stop=params.stop,
- logprobs=False,
- seed=params.seed,
- stream=False,
- presence_penalty=self.presence_penalty,
- )
- assert isinstance(api_result, ChatCompletion)
-
- results = []
- for data in api_result.choices:
- assert data.message.content is not None
-
- assert data.logprobs is not None and data.logprobs.content is not None
- text = data.message.content
-
- stop_reason = _GROQ_STOP_REASON_TO_STOP_REASON[str(data.finish_reason)]
-
- # Note, like OpenAI, Groq treats end turn and stop sequence the same
- # Here we assume it is stop sequence if user has specified a stop sequence
- if params.stop is not None and stop_reason == ResponseStopReason.END_TURN:
- text += params.stop
- result = LanguageModelResponse(
- text=text,
- token_count=0,
- stop_reason=stop_reason,
- network_failure_count=network_failure_count,
- )
- results.append(result)
-
- logger.trace("text: " + results[0].text)
- if api_result.usage is not None:
- completion_tokens = api_result.usage.completion_tokens
- prompt_tokens = api_result.usage.prompt_tokens
- else:
- completion_tokens = 0
- prompt_tokens = 0
- dollars_used = self.calculate_cost(prompt_tokens, completion_tokens)
- logger.trace("dollars used: {}", dollars_used)
- return CostedLanguageModelResponse(
- usage=LanguageModelResponseUsage(
- prompt_tokens_used=prompt_tokens,
- completion_tokens_used=completion_tokens,
- dollars_used=dollars_used,
- ),
- responses=tuple(results),
- )
-
- async def _get_api_stream(
- self,
- prompt: str,
- params: LanguageModelGenerationParams,
- ) -> AsyncGenerator[LanguageModelStreamEvent, None]:
- with _groq_exception_manager():
- messages = convert_prompt_to_openai_messages(prompt)
- client = self._get_client()
- async with _get_capacity_semaphor(self.model_name):
- api_result = await client.chat.completions.create(
- model=self.external_model_name,
- messages=messages, # type: ignore
- max_tokens=params.max_tokens,
- n=1,
- temperature=params.temperature,
- stop=params.stop,
- logprobs=False,
- seed=params.seed,
- stream=True,
- # This field is currently unsupported by the groq API
- # stream_options={"include_usage": True},
- presence_penalty=self.presence_penalty,
- )
- assert isinstance(api_result, AsyncStream)
- logger.info("API response status code: {}", api_result.response.status_code)
-
- yield LanguageModelStreamStartEvent()
-
- usage = None
- finish_reason: str | None = None
- async for chunk in api_result:
- if chunk.choices:
- assert len(chunk.choices) == 1, "Currently only count=1 supported for streaming API."
- data = only(chunk.choices)
- delta = data.delta.content
- if delta is not None:
- yield LanguageModelStreamDeltaEvent(delta=delta)
- if data.finish_reason:
- finish_reason = str(data.finish_reason)
-
- stop_reason = _GROQ_STOP_REASON_TO_STOP_REASON[str(finish_reason)]
- # Note, Open API treats end turn and stop sequence the same TODO: check if groq is the same
- # Here we assume it is stop sequence if user has specified a stop sequence
- if params.stop is not None and stop_reason == ResponseStopReason.END_TURN:
- yield LanguageModelStreamDeltaEvent(delta=params.stop)
-
- if usage is not None:
- completion_tokens = usage.completion_tokens
- prompt_tokens = usage.prompt_tokens
- dollars_used = self.calculate_cost(prompt_tokens, completion_tokens)
- else:
- completion_tokens = -1
- prompt_tokens = -1
- dollars_used = -1
- logger.trace("dollars used: {}", dollars_used)
-
- yield LanguageModelStreamEndEvent(
- usage=LanguageModelResponseUsage(
- prompt_tokens_used=prompt_tokens,
- completion_tokens_used=completion_tokens,
- dollars_used=dollars_used,
- ),
- stop_reason=stop_reason,
- )
diff --git a/imbue_core/imbue_core/agents/llm_apis/language_model_api.py b/imbue_core/imbue_core/agents/llm_apis/language_model_api.py
@@ -1,547 +0,0 @@
-import abc
-import asyncio
-import contextvars
-import hashlib
-import inspect
-import os
-import random
-from pathlib import Path
-from types import FrameType
-from typing import AsyncGenerator
-from typing import Awaitable
-from typing import Callable
-from typing import TypeVar
-from typing import final
-from uuid import UUID
-from uuid import uuid4
-
-import anyio
-from loguru import logger
-
-from imbue_core.agents.llm_apis.constants import approximate_token_count
-from imbue_core.agents.llm_apis.data_types import CachedCostedLanguageModelResponse
-from imbue_core.agents.llm_apis.data_types import CachedCostedModelResponse
-from imbue_core.agents.llm_apis.data_types import CachingInfo
-from imbue_core.agents.llm_apis.data_types import CostedLanguageModelResponse
-from imbue_core.agents.llm_apis.data_types import CountTokensResponse
-from imbue_core.agents.llm_apis.data_types import InputsT
-from imbue_core.agents.llm_apis.data_types import LanguageModelCompleteInputs
-from imbue_core.agents.llm_apis.data_types import LanguageModelGenerationParams
-from imbue_core.agents.llm_apis.data_types import LanguageModelResponse
-from imbue_core.agents.llm_apis.data_types import LanguageModelStreamInputs
-from imbue_core.agents.llm_apis.data_types import ModelResponseT
-from imbue_core.agents.llm_apis.errors import LanguageModelRetryLimitError
-from imbue_core.agents.llm_apis.errors import PromptTooLongError
-from imbue_core.agents.llm_apis.errors import TransientLanguageModelError
-from imbue_core.agents.llm_apis.errors import UnsetCachePathError
-from imbue_core.agents.llm_apis.models import ModelInfo
-from imbue_core.agents.llm_apis.stream import LanguageModelStreamCallback
-from imbue_core.agents.llm_apis.stream import LanguageModelStreamEvent
-from imbue_core.agents.llm_apis.stream import PromptDebuggingCallback
-from imbue_core.agents.llm_apis.stream import SettleSpendCallback
-from imbue_core.agents.llm_apis.stream import StreamedLanguageModelResponse
-from imbue_core.agents.llm_apis.stream import UpdateCacheCallback
-from imbue_core.agents.llm_apis.stream import get_cached_response_stream
-from imbue_core.agents.primitives.resource_limits import PaymentAuthorization
-from imbue_core.agents.primitives.resource_limits import get_global_resource_limits
-from imbue_core.async_utils import sync
-from imbue_core.caching import AsyncCache
-from imbue_core.cattrs_serialization import serialize_to_json
-from imbue_core.pydantic_serialization import MutableModel
-
-# Context variable to disable caching.
-IS_LLM_CACHING_DISABLED_GLOBALLY = contextvars.ContextVar("is_llm_caching_disabled_globally", default=False)
-# Context variable for injecting a default seed which will become part of the LLM cache key.
-LLM_GLOBAL_DEFAULT_SEED = contextvars.ContextVar("llm_global_default_seed", default=0)
-
-# Maximum number of retries for network failures.
-MAX_RETRIES = 5
-
-
-EXCLUDED_CACHE_KEY_ARGS = ["self", "is_caching_enabled", "call_id"]
-
-
-def _create_base_cache_key_from_frame(frame: FrameType) -> str:
- """Create a cache key from the args of a function by passing its frame."""
- args, _, _, values = inspect.getargvalues(frame)
- return "|".join(f"{arg}={values[arg]}" for arg in args if arg not in EXCLUDED_CACHE_KEY_ARGS)
-
-
-CostedResponseT = TypeVar("CostedResponseT", bound=CostedLanguageModelResponse | CountTokensResponse)
-FinalResponseT = TypeVar(
- "FinalResponseT",
- bound=CostedLanguageModelResponse | StreamedLanguageModelResponse | CountTokensResponse,
-)
-
-
-class LanguageModelAPI(abc.ABC, MutableModel):
- model_name: str
- cache_path: Path | None
- is_caching_inputs: bool = False
- is_running_offline: bool = False
- is_conversational: bool = False
- is_using_logprobs: bool = False
-
- # retry/timeout values
- retry_sleep_time: float = 2.0
- retry_backoff_factor: float = 3.0
- retry_jitter_factor: float = 0.5
-
- # TODO: Consider storing the model_config here as well.
-
- @property
- @abc.abstractmethod
- def model_info(self) -> ModelInfo: ...
-
- def get_response_cache(self) -> AsyncCache[CachedCostedLanguageModelResponse]:
- if self.cache_path is None:
- raise UnsetCachePathError()
- return AsyncCache(self.cache_path, CachedCostedLanguageModelResponse)
-
- def _create_cache_key(self, base_key: str) -> str:
- object_cache_attributes = self.model_dump(
- exclude={
- "cache_path",
- "count_tokens_cache_path",
- "base_url",
- "api_key_env",
- "context_window",
- "max_output_tokens",
- }
- )
- # have to reset the offline key to the same value so that that doesnt invalidate the cache
- object_cache_attributes["is_running_offline"] = True
- object_cache_attributes["__name__"] = self.__class__.__name__
- base_key_md5 = hashlib.md5(base_key.encode()).hexdigest()
- object_cache_attributes["__request_key_md5__"] = base_key_md5
- return serialize_to_json(object_cache_attributes)
-
- async def check_cache_core(
- self,
- cache_getter: Callable[[], AsyncCache[CachedCostedModelResponse[InputsT, ModelResponseT]]],
- cache_key: str,
- ) -> ModelResponseT | None:
- async with cache_getter() as cache:
- cached_result = await cache.get(cache_key)
-
- if cached_result is not None:
- if cached_result.error:
- if cached_result.error.startswith(PromptTooLongError.__name__):
- raise PromptTooLongError.from_string(cached_result.error)
- raise Exception(f"Unknown cached result error type: {cached_result.error}")
- assert cached_result.response is not None
- return cached_result.response
- return None
-
- async def check_cache(self, cache_key: str) -> CostedLanguageModelResponse | None:
- return await self.check_cache_core(self.get_response_cache, cache_key)
-
- async def _get_auth(self, prompt: str, max_tokens: int | None) -> PaymentAuthorization | None:
- global_resource_limits = get_global_resource_limits()
- if global_resource_limits is not None:
- prompt_tokens = self.count_tokens(prompt)
- completion_tokens = max_tokens if max_tokens is not None else self.get_max_completion_size_in_tokens()
- upper_bound_cost_estimate = self.estimate_cost(prompt_tokens, completion_tokens)
- assert global_resource_limits is not None
- auth: PaymentAuthorization = await global_resource_limits.authorize_spend(
- upper_bound_cost_estimate,
- debug_info={
- "model_name": self.model_name,
- "prompt_tokens": prompt_tokens,
- "completion_tokens": completion_tokens,
- },
- )
- return auth
-
- if "PYTEST_CURRENT_TEST" not in os.environ:
- logger.warning(
- "You are trying to call a language model with no global resource limits set. That is a bad idea because the spend will not be restricted, and you may end up accidentally spending much more than you expected."
- )
- return None
-
- async def _settle_spend(self, auth: PaymentAuthorization, dollars_used: float) -> None:
- global_resource_limits = get_global_resource_limits()
- assert global_resource_limits is not None
- await global_resource_limits.settle_spend(auth, dollars_used)
- return None
-
- def assert_caching_enabled_if_offline(self, is_caching_enabled: bool) -> None:
- if self.is_running_offline:
- assert is_caching_enabled, "Caching must be enabled when running offline"
-
- def assert_not_offline_if_cache_miss(self, prompt: str) -> None:
- max_n_chars = 50
- prompt_stub = prompt[:max_n_chars] + ("..." if len(prompt) > max_n_chars else "")
- assert (
- not self.is_running_offline
- ), f"Running offline but did not have a cached response for this query! Prompt: {prompt_stub}"
-
- async def complete(
- self,
- prompt: str,
- params: LanguageModelGenerationParams,
- is_caching_enabled: bool = True,
- ) -> tuple[LanguageModelResponse, ...]:
- call_id = uuid4()
- logger.trace(
- "[{call_id}] Calling complete with params: {params} and is_caching_enabled={is_caching_enabled} and {prompt}",
- call_id=call_id,
- params=params,
- is_caching_enabled=is_caching_enabled,
- prompt=prompt[:40],
- )
- if _complete_concurrency_hook_fn is not None:
- await _complete_concurrency_hook_fn(self)
-
- is_caching_enabled_with_override = is_caching_enabled and not IS_LLM_CACHING_DISABLED_GLOBALLY.get()
- if params.seed is None:
- params = params.evolve(params.ref().seed, LLM_GLOBAL_DEFAULT_SEED.get())
-
- return await self._complete(
- prompt,
- params,
- is_caching_enabled=is_caching_enabled_with_override,
- call_id=call_id,
- )
-
- complete_sync = sync(complete)
-
- async def complete_with_usage(
- self,
- prompt: str,
- params: LanguageModelGenerationParams,
- is_caching_enabled: bool = True,
- ) -> CostedLanguageModelResponse:
- call_id = uuid4()
- if _complete_concurrency_hook_fn is not None:
- await _complete_concurrency_hook_fn(self)
-
- is_caching_enabled_with_override = is_caching_enabled and not IS_LLM_CACHING_DISABLED_GLOBALLY.get()
- if params.seed is None:
- params = params.evolve(params.ref().seed, LLM_GLOBAL_DEFAULT_SEED.get())
-
- return await self._complete_with_usage(
- prompt,
- params,
- is_caching_enabled=is_caching_enabled_with_override,
- call_id=call_id,
- )
-
- complete_with_usage_sync = sync(complete_with_usage)
-
- async def _complete_with_usage(
- self,
- prompt: str,
- params: LanguageModelGenerationParams,
- is_caching_enabled: bool,
- call_id: UUID,
- ) -> CostedLanguageModelResponse:
- self._warn_if_no_stop_condition_and_not_conversational(params)
- self.assert_caching_enabled_if_offline(is_caching_enabled)
-
- frame: FrameType | None = None
- if is_caching_enabled:
- frame = inspect.currentframe()
-
- costed_response_to_output: Callable[[CostedLanguageModelResponse], CostedLanguageModelResponse] = lambda cr: cr
-
- cache_key: str | None = None
- if is_caching_enabled:
- cache_key, cached_response = await self._get_from_cache(frame, costed_response_to_output)
-
- if cached_response is not None:
- return cached_response
-
- self.assert_not_offline_if_cache_miss(prompt)
-
- auth = await self._get_auth(prompt, params.max_tokens)
-
- sleep_time = self.retry_sleep_time
- last_error_msg: str | None = None
- for network_failure_count in range(MAX_RETRIES):
- try:
- api_inputs = LanguageModelCompleteInputs(
- prompt=prompt,
- params=params,
- network_failure_count=network_failure_count,
- )
- response = await self._call_api_one_arg(api_inputs)
- if is_caching_enabled:
- assert cache_key is not None
- result = CachedCostedLanguageModelResponse(
- response=response,
- inputs=api_inputs if self.is_caching_inputs else None,
- )
- async with self.get_response_cache() as cache:
- await cache.set(cache_key, result)
-
- if auth is not None:
- await self._settle_spend(auth, response.usage.dollars_used)
-
- return response
-
- except PromptTooLongError as e:
- logger.trace(
- "[{call_id}] Prompt too long error in model {model_name}",
- call_id=call_id,
- model_name=self.model_name,
- )
- if is_caching_enabled:
- assert cache_key is not None
- async with self.get_response_cache() as cache:
- await cache.set(
- cache_key,
- CachedCostedLanguageModelResponse(error=e.to_string()),
- )
- raise
- except TransientLanguageModelError as e:
- last_error_msg = str(e)
- if network_failure_count < MAX_RETRIES - 1:
- if self.retry_jitter_factor > 0:
- max_jitter = sleep_time * self.retry_jitter_factor
- sleep_time += random.uniform(-max_jitter / 2, max_jitter / 2)
- logger.debug(
- f"Transient language model error ({str(e)}) in model {self.model_name}, retrying with sleep time {sleep_time} seconds..."
- )
- await asyncio.sleep(sleep_time)
- sleep_time *= self.retry_backoff_factor
- raise LanguageModelRetryLimitError(last_error_msg or "Unknown error (this should not happen)")
-
- async def _complete(
- self,
- prompt: str,
- params: LanguageModelGenerationParams,
- is_caching_enabled: bool,
- call_id: UUID,
- ) -> tuple[LanguageModelResponse, ...]:
- # Delegate to _complete_with_usage and extract just the responses
- # May have more than count responses cached, so just return first count responses
- costed_response = await self._complete_with_usage(prompt, params, is_caching_enabled, call_id)
- return costed_response.responses[: params.count]
-
- @final
- async def _call_api_one_arg(self, api_inputs: LanguageModelCompleteInputs) -> CostedLanguageModelResponse:
- """Delegates to the abstract method _call_api, which must be implemented by subclasses."""
- return await self._call_api(
- prompt=api_inputs.prompt,
- params=api_inputs.params,
- network_failure_count=api_inputs.network_failure_count,
- )
-
- @abc.abstractmethod
- async def _call_api(
- self,
- prompt: str,
- params: LanguageModelGenerationParams,
- # this is used to track how many times we've retried due to network failures, since we want the return type to contain that information
- network_failure_count: int = 0,
- ) -> CostedLanguageModelResponse:
- """If defined, the stop sequence should be part of the sequence (if it was actually generated)"""
-
- async def stream(
- self,
- prompt: str,
- is_caching_enabled: bool = True,
- params: LanguageModelGenerationParams = LanguageModelGenerationParams(),
- ) -> StreamedLanguageModelResponse:
- if params.seed is None:
- params = params.evolve(params.ref().seed, LLM_GLOBAL_DEFAULT_SEED.get())
-
- is_caching_enabled_with_override = is_caching_enabled and not IS_LLM_CACHING_DISABLED_GLOBALLY.get()
-
- return await self._stream(
- prompt=prompt,
- is_caching_enabled=is_caching_enabled_with_override,
- params=params,
- )
-
- async def _stream(
- self,
- prompt: str,
- is_caching_enabled: bool,
- params: LanguageModelGenerationParams,
- ) -> StreamedLanguageModelResponse:
- assert params.count == 1, "Stream API currently only supports count=1 due to limitations of some APIs."
-
- self._warn_if_no_stop_condition_and_not_conversational(params)
- self.assert_caching_enabled_if_offline(is_caching_enabled)
-
- frame: FrameType | None = None
- if is_caching_enabled:
- frame = inspect.currentframe()
-
- # Note it's technically possible multiple responses cached for given prompt (e.g. from call to complete())
- # for now we just return first one
- costed_response_to_output = lambda cr: StreamedLanguageModelResponse(
- get_cached_response_stream(cr),
- network_failure_count=0,
- completion_callbacks=(),
- )
- cache_key: str | None = None
- if is_caching_enabled:
- cache_key, cached_response = await self._get_from_cache(frame, costed_response_to_output)
-
- if cached_response is not None:
- return cached_response
-
- self.assert_not_offline_if_cache_miss(prompt)
-
- auth = await self._get_auth(prompt, params.max_tokens)
-
- sleep_time = self.retry_sleep_time
- last_error_msg: str | None = None
- for network_failure_count in range(MAX_RETRIES):
- # Loop until success or an exception is raised
- try:
- api_inputs = LanguageModelStreamInputs(prompt=prompt, params=params)
- api_stream = await self._get_api_stream_one_arg(api_inputs)
- callbacks: list[LanguageModelStreamCallback] = []
- if is_caching_enabled:
- assert cache_key is not None
- cache = self.get_response_cache()
- callbacks.append(
- UpdateCacheCallback(
- key=cache_key,
- cache=cache,
- api_inputs=api_inputs if self.is_caching_inputs else None,
- )
- )
- llm_debug_output_folder = os.getenv("LLM_DEBUG_PATH", None)
- if llm_debug_output_folder is not None:
- output_path = anyio.Path(llm_debug_output_folder) / f"{uuid4()}.txt"
- # write out the prompt (helps with debugging so we can see when things blow up)
- await output_path.write_text(prompt)
- # overwrite the file with the prompt and completion when done
- callbacks.append(PromptDebuggingCallback(prompt=prompt, output_path=output_path))
-
- if auth is not None:
- callbacks.append(SettleSpendCallback(auth=auth))
-
- return StreamedLanguageModelResponse(
- api_stream,
- network_failure_count=network_failure_count,
- completion_callbacks=callbacks,
- )
-
- except PromptTooLongError as e:
- if is_caching_enabled:
- assert cache_key is not None
- async with self.get_response_cache() as cache:
- await cache.set(
- cache_key,
- CachedCostedLanguageModelResponse(error=e.to_string()),
- )
- raise
- except TransientLanguageModelError as e:
- last_error_msg = str(e)
- if network_failure_count < MAX_RETRIES - 1:
- if self.retry_jitter_factor > 0:
- sleep_time += random.uniform(0, sleep_time * self.retry_jitter_factor)
- logger.debug(
- f"Transient language model error ({str(e)}) in model {self.model_name}, retrying with sleep time {sleep_time} seconds..."
- )
- await asyncio.sleep(sleep_time)
- sleep_time *= self.retry_backoff_factor
- raise LanguageModelRetryLimitError(last_error_msg or "Unknown error (this should not happen)")
-
- @final
- async def _get_api_stream_one_arg(
- self, api_inputs: LanguageModelStreamInputs
- ) -> AsyncGenerator[LanguageModelStreamEvent, None]:
- """Delegates to the abstract method _get_api_stream, which must be implemented by subclasses."""
- return self._get_api_stream(prompt=api_inputs.prompt, params=api_inputs.params)
-
- @abc.abstractmethod
- def _get_api_stream(
- self,
- prompt: str,
- params: LanguageModelGenerationParams,
- ) -> AsyncGenerator[LanguageModelStreamEvent, None]:
- """If defined, the stop sequence should be part of the sequence (if it was actually generated)"""
-
- def _warn_if_no_stop_condition_and_not_conversational(self, params: LanguageModelGenerationParams) -> None:
- if (params.stop is None and params.max_tokens is None) and not self.is_conversational:
- logger.debug(
- "Did not specify either `max_tokens` or `stop`, and this is not a conversational model. The completion will go until the entire context window is filled. Preferably you don't do this, because it is fairly inefficient."
- )
-
- async def _get_from_cache_core(
- self,
- frame: FrameType | None,
- costed_response_to_output: Callable[[CostedResponseT], FinalResponseT],
- cache_checker: Callable[[str], Awaitable[CostedResponseT | None]],
- ) -> tuple[str | None, FinalResponseT | None]:
- cache_key: str | None
-
- cache_key, costed_response = await self._get_costed_response_from_frame_core(cache_checker, frame)
-
- if costed_response is not None:
- return cache_key, costed_response_to_output(costed_response)
- return cache_key, None
-
- async def _get_from_cache(
- self,
- frame: FrameType | None,
- costed_response_to_output: Callable[[CostedLanguageModelResponse], FinalResponseT],
- ) -> tuple[str | None, FinalResponseT | None]:
- return await self._get_from_cache_core(frame, costed_response_to_output, self.check_cache)
-
- async def _get_costed_response_from_frame_core(
- self,
- cache_checker: Callable[[str], Awaitable[CostedResponseT | None]],
- frame: FrameType | None,
- ) -> tuple[str, CostedResponseT | None]:
- assert frame is not None
- cache_key = self._create_cache_key(_create_base_cache_key_from_frame(frame))
- costed_response = await cache_checker(cache_key)
- return cache_key, costed_response
-
- def count_tokens(self, text: str) -> int:
- # this is VERY approximate, but many of the child models have nothing, so...
- return approximate_token_count(text)
-
- def basic_calculate_cost(self, prompt_tokens: int, completion_tokens: int) -> float:
- return (
- prompt_tokens * self.model_info.cost_per_input_token
- + completion_tokens * self.model_info.cost_per_output_token
- )
-
- def estimate_cost(self, prompt_tokens: int, completion_tokens: int) -> float:
- """Estimate the cost of a request before it has been made. Doesn't use any caching info."""
- return self.basic_calculate_cost(prompt_tokens, completion_tokens)
-
- def calculate_cost(
- self,
- prompt_tokens: int,
- completion_tokens: int,
- caching_info: CachingInfo | None = None,
- ) -> float:
- """Overridden by subclasses which have more complex cost calculations, such as if caching is used."""
- logger.info(
- f"no calculate_cost implemented for {self.model_name}; using basic_calculate_cost",
- model_name=self.model_name,
- )
- return self.basic_calculate_cost(prompt_tokens, completion_tokens)
-
- def get_max_completion_size_in_tokens(self) -> int:
- if self.model_info.max_output_tokens is not None:
- return self.model_info.max_output_tokens
- # assume max output is just the context window size
- return self.model_info.max_input_tokens
-
- def get_max_prompt_size_in_tokens(self) -> int:
- return self.model_info.max_input_tokens
-
- def get_context_window_size_in_tokens(self) -> int:
- return self.get_max_completion_size_in_tokens() + self.get_max_prompt_size_in_tokens()
-
-
-COMPLETE_CONCURRENCY_HOOK_FN = Callable[[LanguageModelAPI], Awaitable[None]] | None
-_complete_concurrency_hook_fn: COMPLETE_CONCURRENCY_HOOK_FN = None
-
-
-def set_language_model_api_complete_concurrency_hook(
- hook_fn: COMPLETE_CONCURRENCY_HOOK_FN,
-) -> None:
- global _complete_concurrency_hook_fn
- _complete_concurrency_hook_fn = hook_fn
diff --git a/imbue_core/imbue_core/agents/llm_apis/mock_api.py b/imbue_core/imbue_core/agents/llm_apis/mock_api.py
@@ -1,193 +0,0 @@
-import asyncio
-import enum
-from pathlib import Path
-from typing import AsyncGenerator
-
-import toml
-from loguru import logger
-from pydantic.fields import Field
-from pydantic.functional_validators import field_validator
-
-from imbue_core.agents.llm_apis.data_types import CostedLanguageModelResponse
-from imbue_core.agents.llm_apis.data_types import LanguageModelGenerationParams
-from imbue_core.agents.llm_apis.data_types import LanguageModelResponse
-from imbue_core.agents.llm_apis.data_types import LanguageModelResponseUsage
-from imbue_core.agents.llm_apis.data_types import LanguageModelResponseWithLogits
-from imbue_core.agents.llm_apis.data_types import ResponseStopReason
-from imbue_core.agents.llm_apis.data_types import TokenProbability
-from imbue_core.agents.llm_apis.language_model_api import LanguageModelAPI
-from imbue_core.agents.llm_apis.models import ModelInfo
-from imbue_core.agents.llm_apis.stream import LanguageModelStreamDeltaEvent
-from imbue_core.agents.llm_apis.stream import LanguageModelStreamEndEvent
-from imbue_core.agents.llm_apis.stream import LanguageModelStreamEvent
-from imbue_core.itertools import only
-from imbue_core.pydantic_serialization import MutableModel
-
-
-class MockModelName(enum.StrEnum):
- MOCK_MODEL = "my-mock-model"
-
-
-MY_MOCK_MODEL_INFO = ModelInfo(
- model_name=MockModelName.MOCK_MODEL,
- cost_per_input_token=0.0 / 1_000_000,
- cost_per_output_token=0.0 / 1_000_000,
- max_input_tokens=32_768,
- max_output_tokens=None,
-)
-
-
-class Stats(MutableModel):
- complete_calls: int = 0
-
-
-class LanguageModelMock(LanguageModelAPI):
- model_name: str = MY_MOCK_MODEL_INFO.model_name
- cache_path: Path | None = None
- # FIXME: can't have a mutable class inside a frozen pydantic model
- stats: Stats = Field(default_factory=Stats)
-
- @property
- def model_info(self) -> ModelInfo:
- return MY_MOCK_MODEL_INFO
-
- async def complete(
- self,
- prompt: str,
- params: LanguageModelGenerationParams,
- is_caching_enabled: bool = True,
- ) -> tuple[LanguageModelResponse, ...]:
- raise NotImplementedError()
-
- def _get_token_probabilities(self, response_text: str) -> tuple[tuple[TokenProbability, ...], ...]:
- return tuple(
- (TokenProbability(token=pseudo_token, log_probability=0.0, is_stop=False),)
- for pseudo_token in response_text.split()
- )
-
- async def _call_api(
- self,
- prompt: str,
- params: LanguageModelGenerationParams,
- network_failure_count: int = 0,
- ) -> CostedLanguageModelResponse:
- raise NotImplementedError()
-
- def _get_api_stream(
- self,
- prompt: str,
- params: LanguageModelGenerationParams,
- ) -> AsyncGenerator[LanguageModelStreamEvent, None]:
- # TODO Implement streaming support (?)
- raise NotImplementedError()
-
-
-MOCK_STREAM_SLEEP_TIME = 5.0
-
-
-class FileBasedLanguageModelMock(LanguageModelMock):
- """
- A mock LLM API that reads responses from a toml file.
- The response can either be identified using the toml key or a prompt which is part of the toml dictionary.
- """
-
- calls: int = 0
-
- @field_validator("cache_path") # pyre-ignore[56]: pyre doesn't understand pydantic
- @classmethod
- def validate_cache_path(cls, v: Path | None) -> Path | None:
- if v is None:
- raise ValueError("Mock responses file path is not set.")
- if not v.exists():
- raise ValueError(f"Mock responses file {v} does not exist.")
- if not v.suffix == ".toml":
- raise ValueError(f"Mock responses file {v} is not a toml file.")
- return v
-
- def _get_user_message_from_prompt(self, prompt: str) -> str:
- user_prompt = prompt.rsplit("[ROLE=USER]", 1)[-1].strip()
- return user_prompt
-
- def get_single_response(self, prompt: str) -> str:
- return only(self.get_parts_of_response(prompt))
-
- def get_parts_of_response(self, prompt: str) -> tuple[str, ...]:
- """
- Support both of the following possible identifiers:
- [identifier]
- prompt = "user message here"
- [[identifier.responses]]
- text = "response"
- [[identifier.responses]]
- text = "response2"
-
- [identifier]
- prompt = "user message here"
- response = "response"
- """
- # this is checked during validation but i guess the type checker doesn't see it
- assert self.cache_path is not None
- # TODO: should we try something that is not toml? toml formatting is a little annoying
- toml_dict = toml.load(self.cache_path)
- # TODO: currently the identifier is the last user message, because the entire prompt is really long
- # if we need to support the same user message with different responses, expand this, maybe chat history?
- identifier = self._get_user_message_from_prompt(prompt)
- logger.info("Getting response for identifier: {} from {}", identifier, toml_dict)
- toml_item = toml_dict.get(identifier, None)
- if toml_item is None:
- for toml_key, response in toml_dict.items():
- if "prompt" in response:
- if response["prompt"] == identifier:
- toml_item = response
- break
- if toml_item is None:
- raise KeyError(f"No response found for the given identifier {identifier}")
-
- if "responses" in toml_item:
- responses = toml_item["responses"]
- if isinstance(responses, list):
- return tuple(r["text"] for r in responses if isinstance(r, dict) and "text" in r)
- raise ValueError(f"Expected 'responses' to be a list of tables in section '{identifier}'")
-
- if "response" in toml_item:
- return (str(toml_item["response"]),)
-
- raise ValueError(f"No valid response or responses found for identifier '{identifier}'")
-
- async def complete(
- self,
- prompt: str,
- params: LanguageModelGenerationParams,
- is_caching_enabled: bool = True,
- ) -> tuple[LanguageModelResponse, ...]:
- response = self.get_single_response(prompt)
- self.stats.complete_calls += 1
- token_probabilities = self._get_token_probabilities(response)
- return (
- LanguageModelResponseWithLogits(
- text=response,
- token_count=len(token_probabilities),
- stop_reason=ResponseStopReason.NONE,
- network_failure_count=0,
- token_probabilities=token_probabilities,
- ),
- )
-
- async def _get_api_stream(
- self,
- prompt: str,
- params: LanguageModelGenerationParams,
- ) -> AsyncGenerator[LanguageModelStreamEvent, None]:
- responses = self.get_parts_of_response(prompt)
- self.stats.complete_calls += 1
- if len(responses) == 1:
- response = responses[0]
- yield LanguageModelStreamDeltaEvent(delta=response)
- else:
- for response in responses:
- yield LanguageModelStreamDeltaEvent(delta=response)
- await asyncio.sleep(MOCK_STREAM_SLEEP_TIME)
- yield LanguageModelStreamEndEvent(
- usage=LanguageModelResponseUsage(prompt_tokens_used=0, completion_tokens_used=0, dollars_used=0),
- stop_reason=ResponseStopReason.NONE,
- )
diff --git a/imbue_core/imbue_core/agents/llm_apis/models.py b/imbue_core/imbue_core/agents/llm_apis/models.py
@@ -1,17 +0,0 @@
-from imbue_core.agents.llm_apis.union_data_types import ProviderSpecificModelInfoUnion
-from imbue_core.pydantic_serialization import SerializableModel
-
-
-class ModelInfo(SerializableModel):
- model_name: str
- cost_per_input_token: float
- cost_per_output_token: float
- max_input_tokens: int
- max_output_tokens: int | None
- # requests per second
- rate_limit_req: float | None = None
- # tokens per second
- rate_limit_tok: float | None = None
- rate_limit_output_tok: float | None = None
- max_thinking_budget: int | None = None
- provider_specific_info: ProviderSpecificModelInfoUnion | None = None
diff --git a/imbue_core/imbue_core/agents/llm_apis/openai_api.py b/imbue_core/imbue_core/agents/llm_apis/openai_api.py
@@ -1,654 +0,0 @@
-import asyncio
-import enum
-import re
-from collections import defaultdict
-from contextlib import contextmanager
-from functools import lru_cache
-from typing import AsyncGenerator
-from typing import Iterator
-from typing import Mapping
-
-import httpx
-import tiktoken
-from loguru import logger
-from openai import AsyncStream
-from openai import InternalServerError
-from openai import NOT_GIVEN
-from openai import NotGiven
-from openai._client import AsyncOpenAI
-from openai._exceptions import APIConnectionError
-from openai._exceptions import BadRequestError
-from openai._exceptions import RateLimitError
-from openai.types.chat import ChatCompletion
-from pydantic.functional_validators import field_validator
-
-from imbue_core.agents.llm_apis.api_utils import convert_prompt_to_openai_messages
-from imbue_core.agents.llm_apis.data_types import CachingInfo
-from imbue_core.agents.llm_apis.data_types import CostedLanguageModelResponse
-from imbue_core.agents.llm_apis.data_types import LanguageModelGenerationParams
-from imbue_core.agents.llm_apis.data_types import LanguageModelResponse
-from imbue_core.agents.llm_apis.data_types import LanguageModelResponseUsage
-from imbue_core.agents.llm_apis.data_types import LanguageModelResponseWithLogits
-from imbue_core.agents.llm_apis.data_types import ResponseStopReason
-from imbue_core.agents.llm_apis.data_types import TokenProbability
-from imbue_core.agents.llm_apis.errors import BadAPIRequestError
-from imbue_core.agents.llm_apis.errors import LanguageModelInvalidModelNameError
-from imbue_core.agents.llm_apis.errors import MissingAPIKeyError
-from imbue_core.agents.llm_apis.errors import PromptTooLongError
-from imbue_core.agents.llm_apis.errors import TransientLanguageModelError
-from imbue_core.agents.llm_apis.models import ModelInfo
-from imbue_core.agents.llm_apis.openai_compatible_api import OpenAICompatibleAPI
-from imbue_core.agents.llm_apis.openai_compatible_api import (
- _OPENAI_COMPATIBLE_STOP_REASON_TO_STOP_REASON,
-)
-from imbue_core.agents.llm_apis.openai_data_types import OpenAICachingInfo
-from imbue_core.agents.llm_apis.stream import LanguageModelStreamDeltaEvent
-from imbue_core.agents.llm_apis.stream import LanguageModelStreamEndEvent
-from imbue_core.agents.llm_apis.stream import LanguageModelStreamEvent
-from imbue_core.agents.llm_apis.stream import LanguageModelStreamStartEvent
-from imbue_core.frozen_utils import FrozenDict
-from imbue_core.frozen_utils import FrozenMapping
-from imbue_core.itertools import only
-from imbue_core.secrets_utils import get_secret
-
-# note: we require that these model versions are explicit, just like the rest of our dependencies
-# the reason is that these models are actually now mostly deterministic, and it is much easier to debug if we know what model was used
-# also, there's no need to troll yourself by wondering why results have improved (or gotten worse) when you dont realized that the version has shifted under you
-# if you want to use an upgraded model, just upgrade the model to the key displayed here: https://platform.openai.com/docs/models/overview
-# please do NOT set these back to the generic model names!
-
-FINE_TUNED_GPT4O_MINI_2024_07_18_PREFIX = "ft:gpt-4o-mini-2024-07-18"
-FINE_TUNED_GPT4O_2024_08_06_PREFIX = "ft:gpt-4o-2024-08-06"
-
-
-class OpenAIModelName(enum.StrEnum):
- GPT_3_5_TURBO = "gpt-3.5-turbo-0125"
- GPT_4_0613 = "gpt-4-0613"
- GPT_4_1106_PREVIEW = "gpt-4-1106-preview"
- GPT_4_0125_PREVIEW = "gpt-4-0125-preview"
- GPT_4_TURBO_2024_04_09 = "gpt-4-turbo-2024-04-09"
- GPT_4O_2024_05_13 = "gpt-4o-2024-05-13"
- GPT_4O_2024_08_06 = "gpt-4o-2024-08-06"
- GPT_4O_MINI_2024_07_18 = "gpt-4o-mini-2024-07-18"
- O1_2024_12_17 = "o1-2024-12-17"
- GPT_4_1_2025_04_14 = "gpt-4.1-2025-04-14"
- GPT_4_1_MINI_2025_04_14 = "gpt-4.1-mini-2025-04-14"
- GPT_4_1_NANO_2025_04_14 = "gpt-4.1-nano-2025-04-14"
- O3_2025_04_16 = "o3-2025-04-16"
- O3_MINI_2025_01_31 = "o3-mini-2025-01-31"
- O4_MINI_2025_04_16 = "o4-mini-2025-04-16"
- GPT_5_2025_08_07 = "gpt-5-2025-08-07"
- GPT_5_MINI_2025_08_07 = "gpt-5-mini-2025-08-07"
- GPT_5_NANO_2025_08_07 = "gpt-5-nano-2025-08-07"
- GPT_5_1_2025_11_13 = "gpt-5.1-2025-11-13"
-
-
-# Using Tier 5 rate limits
-# https://platform.openai.com/settings/organization/limits
-
-OPENAI_MODEL_INFO_BY_NAME: FrozenMapping[OpenAIModelName, ModelInfo] = FrozenDict(
- {
- OpenAIModelName.GPT_3_5_TURBO: ModelInfo(
- model_name=str(OpenAIModelName.GPT_3_5_TURBO),
- cost_per_input_token=0.5 / 1_000_000,
- cost_per_output_token=1.5 / 1_000_000,
- max_input_tokens=16_385,
- max_output_tokens=4096,
- rate_limit_req=10000 / 60, # 10000 RPM = 166.67 RPS
- ),
- OpenAIModelName.GPT_4_0613: ModelInfo(
- model_name=str(OpenAIModelName.GPT_4_0613),
- cost_per_input_token=30.0 / 1_000_000,
- cost_per_output_token=60.0 / 1_000_000,
- max_input_tokens=8192,
- max_output_tokens=8192,
- rate_limit_req=10000 / 60, # 10000 RPM = 166.67 RPS
- ),
- OpenAIModelName.GPT_4_1106_PREVIEW: ModelInfo( # Cannot find this model
- model_name=str(OpenAIModelName.GPT_4_1106_PREVIEW),
- cost_per_input_token=10.0 / 1_000_000,
- cost_per_output_token=30.0 / 1_000_000,
- max_input_tokens=128_000,
- max_output_tokens=4096,
- rate_limit_req=10000 / 60, # 10000 RPM = 166.67 RPS
- ),
- OpenAIModelName.GPT_4_0125_PREVIEW: ModelInfo(
- model_name=str(OpenAIModelName.GPT_4_0125_PREVIEW),
- cost_per_input_token=10.0 / 1_000_000,
- cost_per_output_token=30.0 / 1_000_000,
- max_input_tokens=128_000,
- max_output_tokens=4096,
- rate_limit_req=10000 / 60, # 10000 RPM = 166.67 RPS
- ),
- OpenAIModelName.GPT_4_TURBO_2024_04_09: ModelInfo(
- model_name=str(OpenAIModelName.GPT_4_TURBO_2024_04_09),
- cost_per_input_token=10.0 / 1_000_000,
- cost_per_output_token=30.0 / 1_000_000,
- max_input_tokens=128_000,
- max_output_tokens=4096,
- rate_limit_req=10000 / 60, # 10000 RPM = 166.67 RPS
- ),
- OpenAIModelName.GPT_4O_2024_05_13: ModelInfo(
- model_name=str(OpenAIModelName.GPT_4O_2024_05_13),
- cost_per_input_token=5.0 / 1_000_000,
- cost_per_output_token=15.0 / 1_000_000,
- max_input_tokens=128_000,
- max_output_tokens=4096,
- rate_limit_req=10000 / 60, # 10000 RPM = 166.67 RPS
- ),
- OpenAIModelName.GPT_4O_2024_08_06: ModelInfo(
- model_name=str(OpenAIModelName.GPT_4O_2024_08_06),
- cost_per_input_token=2.5 / 1_000_000,
- cost_per_output_token=10.0 / 1_000_000,
- max_input_tokens=128_000,
- max_output_tokens=16_384,
- rate_limit_req=10000 / 60, # 10000 RPM = 166.67 RPS
- ),
- OpenAIModelName.GPT_4O_MINI_2024_07_18: ModelInfo(
- model_name=str(OpenAIModelName.GPT_4O_MINI_2024_07_18),
- cost_per_input_token=0.15 / 1_000_000,
- cost_per_output_token=0.60 / 1_000_000,
- max_input_tokens=128_000,
- max_output_tokens=16_384,
- rate_limit_req=30000 / 60, # 30000 RPM = 500 RPS
- ),
- OpenAIModelName.O1_2024_12_17: ModelInfo(
- model_name=str(OpenAIModelName.O1_2024_12_17),
- cost_per_input_token=15 / 1_000_000,
- cost_per_output_token=60 / 1_000_000,
- max_input_tokens=200_000,
- max_output_tokens=100_000,
- rate_limit_req=10000 / 60, # 10000 RPM = 166.67 RPS
- ),
- OpenAIModelName.GPT_4_1_2025_04_14: ModelInfo(
- model_name=str(OpenAIModelName.GPT_4_1_2025_04_14),
- cost_per_input_token=2 / 1_000_000,
- cost_per_output_token=8 / 1_000_000,
- max_input_tokens=1_047_576,
- max_output_tokens=32_768,
- rate_limit_req=10000 / 60, # 10000 RPM = 166.67 RPS
- ),
- OpenAIModelName.GPT_4_1_MINI_2025_04_14: ModelInfo(
- model_name=str(OpenAIModelName.GPT_4_1_MINI_2025_04_14),
- cost_per_input_token=0.4 / 1_000_000,
- cost_per_output_token=1.6 / 1_000_000,
- max_input_tokens=1_047_576,
- max_output_tokens=32_768,
- rate_limit_req=30000 / 60, # 30000 RPM = 500 RPS
- ),
- OpenAIModelName.GPT_4_1_NANO_2025_04_14: ModelInfo(
- model_name=str(OpenAIModelName.GPT_4_1_NANO_2025_04_14),
- cost_per_input_token=0.1 / 1_000_000,
- cost_per_output_token=0.4 / 1_000_000,
- max_input_tokens=1_047_576,
- max_output_tokens=32_768,
- rate_limit_req=30000 / 60, # 30000 RPM = 500 RPS
- ),
- OpenAIModelName.O4_MINI_2025_04_16: ModelInfo(
- model_name=str(OpenAIModelName.O4_MINI_2025_04_16),
- cost_per_input_token=1.1 / 1_000_000,
- cost_per_output_token=4.4 / 1_000_000,
- max_input_tokens=200_000,
- max_output_tokens=100_000,
- rate_limit_req=30000 / 60, # 30000 RPM = 500 RPS
- ),
- OpenAIModelName.O3_2025_04_16: ModelInfo(
- model_name=str(OpenAIModelName.O3_2025_04_16),
- cost_per_input_token=10 / 1_000_000,
- cost_per_output_token=40 / 1_000_000,
- max_input_tokens=200_000,
- max_output_tokens=100_000,
- rate_limit_req=10000 / 60, # 10000 RPM = 166.67 RPS
- ),
- OpenAIModelName.O3_MINI_2025_01_31: ModelInfo(
- model_name=str(OpenAIModelName.O3_MINI_2025_01_31),
- cost_per_input_token=1.1 / 1_000_000,
- cost_per_output_token=4.4 / 1_000_000,
- max_input_tokens=200_000,
- max_output_tokens=100_000,
- rate_limit_req=30000 / 60, # 30000 RPM = 500 RPS
- ),
- OpenAIModelName.GPT_5_2025_08_07: ModelInfo(
- model_name=str(OpenAIModelName.GPT_5_2025_08_07),
- cost_per_input_token=1.25 / 1_000_000,
- cost_per_output_token=10 / 1_000_000,
- max_input_tokens=400_000,
- max_output_tokens=128_000,
- rate_limit_req=15000 / 60, # 15000 RPM = 250 RPS
- ),
- OpenAIModelName.GPT_5_MINI_2025_08_07: ModelInfo(
- model_name=str(OpenAIModelName.GPT_5_MINI_2025_08_07),
- cost_per_input_token=0.25 / 1_000_000,
- cost_per_output_token=2.00 / 1_000_000,
- max_input_tokens=400_000,
- max_output_tokens=128_000,
- rate_limit_req=30000 / 60, # 30000 RPM = 500 RPS
- ),
- OpenAIModelName.GPT_5_NANO_2025_08_07: ModelInfo(
- model_name=str(OpenAIModelName.GPT_5_NANO_2025_08_07),
- cost_per_input_token=0.05 / 1_000_000,
- cost_per_output_token=0.40 / 1_000_000,
- max_input_tokens=400_000,
- max_output_tokens=128_000,
- rate_limit_req=30000 / 60, # 30000 RPM = 500 RPS
- ),
- OpenAIModelName.GPT_5_1_2025_11_13: ModelInfo(
- model_name=str(OpenAIModelName.GPT_5_1_2025_11_13),
- cost_per_input_token=1.25 / 1_000_000,
- cost_per_output_token=10 / 1_000_000,
- max_input_tokens=400_000,
- max_output_tokens=128_000,
- rate_limit_req=15000 / 60, # 15000 RPM = 250 RPS
- ),
- }
-)
-
-
-# Pricing for fine-tuned models taken from here: https://platform.openai.com/docs/pricing
-def get_model_info(model_name: OpenAIModelName) -> ModelInfo:
- # Check for the family of fine-tuned models.
- if model_name.startswith(FINE_TUNED_GPT4O_MINI_2024_07_18_PREFIX):
- return ModelInfo(
- model_name=str(model_name),
- cost_per_input_token=0.3 / 1_000_000,
- cost_per_output_token=1.2 / 1_000_000,
- max_input_tokens=128_000,
- max_output_tokens=16_384,
- rate_limit_req=30000 / 60, # 30000 RPM = 500 RPS (same as base model)
- )
- if model_name.startswith(FINE_TUNED_GPT4O_2024_08_06_PREFIX):
- return ModelInfo(
- model_name=str(model_name),
- cost_per_input_token=3.75 / 1_000_000,
- cost_per_output_token=15.0 / 1_000_000,
- max_input_tokens=128_000,
- max_output_tokens=16_384,
- rate_limit_req=10000 / 60, # 10000 RPM = 166.67 RPS (same as base model)
- )
- # Otherwise, return the model info for the base model.
- return OPENAI_MODEL_INFO_BY_NAME[model_name]
-
-
-_CAPACITY_SEMAPHOR_BY_MODEL_NAME: Mapping[OpenAIModelName, asyncio.Semaphore] = defaultdict(
- lambda: asyncio.Semaphore(20),
- {
- OpenAIModelName.GPT_3_5_TURBO: asyncio.Semaphore(100),
- OpenAIModelName.GPT_4_0613: asyncio.Semaphore(60),
- OpenAIModelName.GPT_4_1_NANO_2025_04_14: asyncio.Semaphore(80),
- },
-)
-
-
-def _get_capacity_semaphor(model_name: OpenAIModelName) -> asyncio.Semaphore:
- # Fine-tuned models share rate limits with the base model.
- if model_name.startswith(FINE_TUNED_GPT4O_MINI_2024_07_18_PREFIX):
- model_name = OpenAIModelName.GPT_4O_MINI_2024_07_18
- elif model_name.startswith(FINE_TUNED_GPT4O_2024_08_06_PREFIX):
- model_name = OpenAIModelName.GPT_4O_2024_08_06
- return _CAPACITY_SEMAPHOR_BY_MODEL_NAME[model_name]
-
-
-def is_openai_reasoning_model(model_name: str) -> bool:
- return model_name in (
- OpenAIModelName.O1_2024_12_17,
- OpenAIModelName.O4_MINI_2025_04_16,
- OpenAIModelName.O3_2025_04_16,
- OpenAIModelName.O3_MINI_2025_01_31,
- OpenAIModelName.GPT_5_2025_08_07,
- OpenAIModelName.GPT_5_MINI_2025_08_07,
- OpenAIModelName.GPT_5_NANO_2025_08_07,
- )
-
-
-def is_fine_tuned_openai_model(model_name: OpenAIModelName) -> bool:
- return model_name.value.startswith(FINE_TUNED_GPT4O_MINI_2024_07_18_PREFIX) or model_name.value.startswith(
- FINE_TUNED_GPT4O_2024_08_06_PREFIX
- )
-
-
-_OPENAI_COMPLETION_ERROR_PATTERN = re.compile(
- r".*This model's maximum context length is (\d+) tokens, however you requested (\d+) tokens \((\d+) in your prompt; (\d+) for the completion\). Please reduce your prompt; or completion length.*"
-)
-
-_OPENAI_STOP_REASON_TO_STOP_REASON = _OPENAI_COMPATIBLE_STOP_REASON_TO_STOP_REASON
-
-
-@lru_cache(maxsize=1)
-def get_openai_tokenizer(model_name: str) -> tiktoken.Encoding:
- """Get the appropriate tiktoken tokenizer for an OpenAI model.
-
- Args:
- model_name: The OpenAI model name (e.g., "gpt-4o-2024-08-06").
-
- Returns:
- The tiktoken Encoding for the model.
- """
- if model_name.startswith("gpt-4"):
- fixed_model_name = "gpt-4"
- elif model_name.startswith("gpt-3.5"):
- fixed_model_name = "gpt-3.5"
- else:
- # Just default to `gpt-4o` for now, since this seems to be the most recent tokenizer
- # and we are only using it for estimating token usage
- fixed_model_name = "gpt-4o"
- return tiktoken.encoding_for_model(fixed_model_name)
-
-
-def count_openai_tokens(text: str, model_name: str) -> int:
- return len(get_openai_tokenizer(model_name).encode(text))
-
-
-@contextmanager
-def _openai_exception_manager() -> Iterator[None]:
- """Simple context manager for parsing OpenAI API exceptions."""
- try:
- yield
- except BadRequestError as e:
- error_text_match = _OPENAI_COMPLETION_ERROR_PATTERN.search(str(e))
- if error_text_match is not None:
- max_prompt_len = int(error_text_match.group(1))
- prompt_len = int(error_text_match.group(2))
- logger.debug(
- "PromptTooLongError max_prompt_len={max_prompt_len} prompt_len={prompt_len}",
- max_prompt_len=max_prompt_len,
- prompt_len=prompt_len,
- )
- raise PromptTooLongError(prompt_len, max_prompt_len) from e
- logger.debug("BadAPIRequestError {e}", e=e)
- raise BadAPIRequestError(str(e)) from e
- except APIConnectionError as e:
- logger.debug("Rate limited? Received APIConnectionError {e}", e=e)
- raise TransientLanguageModelError("APIConnectionError") from e
- except RateLimitError as e:
- if e.code == "insufficient_quota":
- raise
- logger.debug("Rate limited? {e}", e=e)
- raise TransientLanguageModelError("RateLimitError") from e
- except httpx.RemoteProtocolError as e:
- logger.debug("httpx.RemoteProtocolError {e}", e=e)
- raise TransientLanguageModelError("httpx.RemoteProtocolError") from e
- except InternalServerError as e:
- logger.debug("InternalServerError {e}", e=e)
- raise TransientLanguageModelError("InternalServerError") from e
-
-
-class OpenAIChatAPI(OpenAICompatibleAPI):
- model_name: OpenAIModelName = OpenAIModelName.GPT_4O_MINI_2024_07_18
-
- @field_validator("model_name") # pyre-ignore[56]: pyre doesn't understand pydantic
- @classmethod
- def validate_model_name(cls, v: str) -> str:
- if v not in OPENAI_MODEL_INFO_BY_NAME:
- raise LanguageModelInvalidModelNameError(v, cls.__name__, list(OPENAI_MODEL_INFO_BY_NAME))
- return v
-
- @property
- def model_info(self) -> ModelInfo:
- return get_model_info(self.model_name)
-
- def _get_client(self) -> AsyncOpenAI:
- api_key = get_secret("OPENAI_API_KEY")
- if not api_key:
- raise MissingAPIKeyError("OPENAI_API_KEY environment variable is not set")
- return AsyncOpenAI( # pyre-ignore[16]: pyre doesn't understand the auto-generated openai._client
- api_key=api_key
- )
-
- async def _call_api(
- self,
- prompt: str,
- params: LanguageModelGenerationParams,
- network_failure_count: int = 0,
- ) -> CostedLanguageModelResponse:
- messages = convert_prompt_to_openai_messages(prompt)
- with _openai_exception_manager():
- client = self._get_client()
-
- is_reasoning_model = is_openai_reasoning_model(self.model_name)
-
- top_logprobs: NotGiven | int
- if self.is_using_logprobs:
- assert not is_reasoning_model, "Logprobs are not supported for reasoning models."
- top_logprobs = 5
- else:
- top_logprobs = NOT_GIVEN
-
- temperature: NotGiven | float = NOT_GIVEN if is_reasoning_model else params.temperature
-
- async with _get_capacity_semaphor(self.model_name):
- api_result = await client.chat.completions.create(
- model=self.model_name,
- messages=messages, # type: ignore
- max_completion_tokens=params.max_tokens,
- n=params.count,
- temperature=temperature,
- stream=False,
- seed=params.seed,
- stop=params.stop,
- presence_penalty=self.presence_penalty,
- logprobs=self.is_using_logprobs,
- top_logprobs=top_logprobs,
- )
- assert isinstance(api_result, ChatCompletion)
-
- usage = api_result.usage
- if usage is not None:
- completion_tokens = usage.completion_tokens
- prompt_tokens = usage.prompt_tokens
- cached_tokens = (
- usage.prompt_tokens_details.cached_tokens if usage.prompt_tokens_details is not None else 0
- ) or 0
- caching_info = CachingInfo(
- read_from_cache=cached_tokens,
- provider_specific_data=OpenAICachingInfo(),
- )
- else:
- completion_tokens = 0
- prompt_tokens = self.count_tokens(prompt)
- cached_tokens = None
- caching_info = None
-
- results: tuple[LanguageModelResponse | LanguageModelResponseWithLogits, ...]
- if self.is_using_logprobs:
- results = self._parse_response_with_logprobs(
- api_result,
- prompt_tokens=prompt_tokens,
- stop=params.stop,
- network_failure_count=network_failure_count,
- )
- else:
- results = self._parse_response_without_logprobs(
- api_result,
- prompt_tokens=prompt_tokens,
- stop=params.stop,
- network_failure_count=network_failure_count,
- )
-
- logger.trace("text: {text}", text=results[0].text)
- dollars_used = self.calculate_cost(prompt_tokens, completion_tokens)
- logger.trace("dollars used: {dollars_used}", dollars_used=dollars_used)
- return CostedLanguageModelResponse(
- usage=LanguageModelResponseUsage(
- prompt_tokens_used=prompt_tokens,
- completion_tokens_used=completion_tokens,
- dollars_used=dollars_used,
- caching_info=caching_info,
- ),
- responses=tuple(results),
- )
-
- async def _get_api_stream(
- self,
- prompt: str,
- params: LanguageModelGenerationParams,
- ) -> AsyncGenerator[LanguageModelStreamEvent, None]:
- messages = convert_prompt_to_openai_messages(prompt)
- with _openai_exception_manager():
- client = self._get_client()
-
- is_reasoning_model = is_openai_reasoning_model(self.model_name)
- temperature: NotGiven | float = NOT_GIVEN if is_reasoning_model else params.temperature
-
- async with _get_capacity_semaphor(self.model_name):
- api_result = await client.chat.completions.create(
- model=self.model_name,
- messages=messages, # type: ignore
- max_completion_tokens=params.max_tokens,
- n=1,
- temperature=temperature,
- stop=params.stop,
- seed=params.seed,
- stream=True,
- stream_options={"include_usage": True},
- presence_penalty=self.presence_penalty,
- logprobs=False, # not used when streaming
- top_logprobs=NOT_GIVEN, # only allowed when logprobs=True
- )
- assert isinstance(api_result, AsyncStream)
-
- yield LanguageModelStreamStartEvent()
-
- usage = None
- finish_reason: str | None = None
- async for chunk in api_result:
- if hasattr(chunk, "usage") and chunk.usage is not None:
- # final chunk containing usage info after all streaming is done
- usage = chunk.usage
- continue
-
- if chunk.choices:
- assert len(chunk.choices) == 1, "Currently only count=1 supported for streaming API."
- data = only(chunk.choices)
- delta = data.delta.content
- if delta is not None:
- yield LanguageModelStreamDeltaEvent(delta=delta)
- if data.finish_reason:
- finish_reason = str(data.finish_reason)
-
- stop_reason = _OPENAI_STOP_REASON_TO_STOP_REASON[str(finish_reason)]
- # Note, OpenAI API treats end turn and stop sequence the same
- # Here we assume it is stop sequence if user has specified a stop sequence
- if params.stop is not None and stop_reason == ResponseStopReason.END_TURN:
- yield LanguageModelStreamDeltaEvent(delta=params.stop)
-
- if usage is not None:
- completion_tokens = usage.completion_tokens
- prompt_tokens = usage.prompt_tokens
- dollars_used = self.calculate_cost(prompt_tokens, completion_tokens)
- cached_tokens = usage.prompt_tokens_details.cached_tokens
- logger.trace(
- "Used this many cached read tokens: {cached_tokens}",
- cached_tokens=cached_tokens,
- )
- caching_info = CachingInfo(
- read_from_cache=cached_tokens,
- provider_specific_data=OpenAICachingInfo(),
- )
- else:
- completion_tokens = -1
- prompt_tokens = -1
- dollars_used = -1
- caching_info = None
- logger.trace("dollars used: {dollars_used}", dollars_used=dollars_used)
-
- yield LanguageModelStreamEndEvent(
- usage=LanguageModelResponseUsage(
- prompt_tokens_used=prompt_tokens,
- completion_tokens_used=completion_tokens,
- dollars_used=dollars_used,
- caching_info=caching_info,
- ),
- stop_reason=stop_reason,
- )
-
- def count_tokens(self, text: str) -> int:
- return count_openai_tokens(text, self.model_name)
-
- def _parse_response_without_logprobs(
- self,
- response: ChatCompletion,
- prompt_tokens: int,
- stop: str | None,
- network_failure_count: int,
- ) -> tuple[LanguageModelResponse, ...]:
- results = []
- for data in response.choices:
- assert data.message.content is not None
- text = data.message.content
- token_count = self.count_tokens(text) + prompt_tokens
- stop_reason = _OPENAI_STOP_REASON_TO_STOP_REASON[str(data.finish_reason)]
- # Note, OpenAI API treats end turn and stop sequence the same
- # Here we assume it is stop sequence if user has specified a stop sequence
- if stop is not None and stop_reason == ResponseStopReason.END_TURN:
- text += stop
- result = LanguageModelResponse(
- text=text,
- token_count=token_count,
- stop_reason=stop_reason,
- network_failure_count=network_failure_count,
- )
- results.append(result)
- return tuple(results)
-
- def _parse_response_with_logprobs(
- self,
- response: ChatCompletion,
- prompt_tokens: int,
- stop: str | None,
- network_failure_count: int,
- ) -> tuple[LanguageModelResponseWithLogits, ...]:
- results = []
- for data in response.choices:
- assert data.message.content is not None
- logprobs = data.logprobs
- assert logprobs is not None
- logprobs_content = logprobs.content
- assert logprobs_content is not None
- text = data.message.content
-
- token_probabilities = []
- for logprob_token_entry in logprobs_content:
- top_logprobs = logprob_token_entry.top_logprobs
- top_entries = [
- TokenProbability(
- token=top_logprob_obj.token,
- log_probability=top_logprob_obj.logprob,
- is_stop=False,
- )
- for top_logprob_obj in top_logprobs
- ]
- selected_entry = TokenProbability(
- token=logprob_token_entry.token,
- log_probability=logprob_token_entry.logprob,
- is_stop=False,
- )
- if selected_entry in top_entries:
- top_entries.remove(selected_entry)
- token_probabilities.append(tuple([selected_entry] + top_entries))
-
- stop_reason = _OPENAI_STOP_REASON_TO_STOP_REASON[str(data.finish_reason)]
-
- # Note, OpenAI API treats end turn and stop sequence the same
- # Here we assume it is stop sequence if user has specified a stop sequence
- if stop is not None and stop_reason == ResponseStopReason.END_TURN:
- text += stop
- token_probabilities.append(
- tuple(
- [
- TokenProbability(
- token=stop,
- log_probability=self.stop_token_log_probability,
- is_stop=True,
- )
- ]
- )
- )
- result = LanguageModelResponseWithLogits(
- text=text,
- token_probabilities=tuple(token_probabilities),
- token_count=len(logprobs_content) + prompt_tokens,
- stop_reason=stop_reason,
- network_failure_count=network_failure_count,
- )
- results.append(result)
- return tuple(results)
diff --git a/imbue_core/imbue_core/agents/llm_apis/openai_compatible_api.py b/imbue_core/imbue_core/agents/llm_apis/openai_compatible_api.py
@@ -1,286 +0,0 @@
-import math
-from contextlib import contextmanager
-from typing import AsyncGenerator
-from typing import Iterator
-
-import httpx
-from loguru import logger
-from openai import AsyncOpenAI
-from openai import AsyncStream
-from openai import InternalServerError
-from openai import NotGiven
-from openai._exceptions import APIConnectionError
-from openai._exceptions import BadRequestError
-from openai._exceptions import RateLimitError
-from openai.types.chat import ChatCompletion
-
-from imbue_core.agents.llm_apis.api_utils import convert_prompt_to_openai_messages
-from imbue_core.agents.llm_apis.constants import approximate_token_count
-from imbue_core.agents.llm_apis.data_types import CachingInfo
-from imbue_core.agents.llm_apis.data_types import CostedLanguageModelResponse
-from imbue_core.agents.llm_apis.data_types import LanguageModelGenerationParams
-from imbue_core.agents.llm_apis.data_types import LanguageModelResponse
-from imbue_core.agents.llm_apis.data_types import LanguageModelResponseUsage
-from imbue_core.agents.llm_apis.data_types import ResponseStopReason
-from imbue_core.agents.llm_apis.errors import BadAPIRequestError
-from imbue_core.agents.llm_apis.errors import PromptTooLongError
-from imbue_core.agents.llm_apis.errors import TransientLanguageModelError
-from imbue_core.agents.llm_apis.language_model_api import LanguageModelAPI
-from imbue_core.agents.llm_apis.models import ModelInfo
-from imbue_core.agents.llm_apis.openai_data_types import OpenAICachingInfo
-from imbue_core.agents.llm_apis.stream import LanguageModelStreamDeltaEvent
-from imbue_core.agents.llm_apis.stream import LanguageModelStreamEndEvent
-from imbue_core.agents.llm_apis.stream import LanguageModelStreamEvent
-from imbue_core.agents.llm_apis.stream import LanguageModelStreamStartEvent
-from imbue_core.frozen_utils import FrozenDict
-from imbue_core.frozen_utils import FrozenMapping
-from imbue_core.itertools import only
-from imbue_core.secrets_utils import get_secret
-
-_OPENAI_COMPATIBLE_STOP_REASON_TO_STOP_REASON: FrozenMapping[str, ResponseStopReason] = FrozenDict(
- {
- "stop": ResponseStopReason.END_TURN,
- "length": ResponseStopReason.MAX_TOKENS,
- "tool_calls": ResponseStopReason.TOOL_CALLS,
- "function_call": ResponseStopReason.FUNCTION_CALL,
- "content_filter": ResponseStopReason.CONTENT_FILTER,
- "None": ResponseStopReason.NONE,
- }
-)
-
-
-# TODO: Should the pre-defined OpenAI model class inherit from this?
-class OpenAICompatibleAPI(LanguageModelAPI):
- model_name: str
- base_url: str = "https://api.openai.com/v1"
- api_key_env: str = "OPENAI_API_KEY"
- context_window: int | None = None
- max_output_tokens: int | None = None
- is_conversational: bool = True
- presence_penalty: float = 0.0
- # this shouldn't really ever even be used, but just in case
- stop_token_log_probability: float = math.log(0.9999)
-
- @property
- def model_info(self) -> ModelInfo:
- if self.context_window is None or self.max_output_tokens is None:
- raise ValueError("Must provide context_window and max_output_tokens, or subclass must override model_info")
- return ModelInfo(
- model_name=self.model_name,
- cost_per_input_token=0.0,
- cost_per_output_token=0.0,
- max_input_tokens=self.context_window,
- max_output_tokens=self.max_output_tokens,
- rate_limit_req=None,
- )
-
- def _get_client(self) -> AsyncOpenAI:
- api_key = get_secret(self.api_key_env) if self.api_key_env else ""
- if not api_key:
- api_key = "not-required"
- logger.debug("API key not set, attempting to use API without key.")
-
- return AsyncOpenAI(
- api_key=api_key,
- base_url=self.base_url,
- )
-
- @contextmanager
- def _exception_handler(self, prompt: str) -> Iterator[None]:
- try:
- yield
- except BadRequestError as e:
- if e.code == "context_length_exceeded":
- prompt_len = self.count_tokens(prompt)
- max_prompt_len = self.model_info.max_input_tokens
- logger.debug(
- "PromptTooLongError max_prompt_len={max_prompt_len} prompt_len={prompt_len}",
- max_prompt_len=max_prompt_len,
- prompt_len=prompt_len,
- )
- raise PromptTooLongError(prompt_len, max_prompt_len) from e
- logger.debug("BadAPIRequestError {e}", e=e)
- raise BadAPIRequestError(str(e)) from e
- except APIConnectionError as e:
- logger.debug("API connection error: {e}", e=e)
- raise TransientLanguageModelError("APIConnectionError") from e
- except RateLimitError as e:
- if e.code == "insufficient_quota":
- raise
- logger.debug("Rate limited: {e}", e=e)
- raise TransientLanguageModelError("RateLimitError") from e
- except httpx.RemoteProtocolError as e:
- logger.debug("httpx.RemoteProtocolError {e}", e=e)
- raise TransientLanguageModelError("httpx.RemoteProtocolError") from e
- except InternalServerError as e:
- logger.debug("InternalServerError {e}", e=e)
- raise TransientLanguageModelError("InternalServerError") from e
-
- async def _call_api(
- self,
- prompt: str,
- params: LanguageModelGenerationParams,
- network_failure_count: int = 0,
- ) -> CostedLanguageModelResponse:
- messages = convert_prompt_to_openai_messages(prompt)
-
- with self._exception_handler(prompt):
- client = self._get_client()
-
- temperature: NotGiven | float = params.temperature
-
- api_result = await client.chat.completions.create(
- model=self.model_name,
- messages=messages,
- max_completion_tokens=params.max_tokens,
- n=params.count,
- temperature=temperature,
- stream=False,
- seed=params.seed,
- stop=params.stop,
- presence_penalty=self.presence_penalty,
- )
- assert isinstance(api_result, ChatCompletion)
-
- usage = api_result.usage
- if usage is not None:
- completion_tokens = usage.completion_tokens
- prompt_tokens = usage.prompt_tokens
- cached_tokens = (
- usage.prompt_tokens_details.cached_tokens if usage.prompt_tokens_details is not None else 0
- ) or 0
- caching_info = CachingInfo(
- read_from_cache=cached_tokens,
- provider_specific_data=OpenAICachingInfo(),
- )
- else:
- completion_tokens = 0
- prompt_tokens = self.count_tokens(prompt)
- cached_tokens = None
- caching_info = None
-
- results = self._parse_response(
- api_result,
- prompt_tokens=prompt_tokens,
- stop=params.stop,
- network_failure_count=network_failure_count,
- )
-
- logger.trace("text: {text}", text=results[0].text)
- dollars_used = self.calculate_cost(prompt_tokens, completion_tokens)
- logger.trace("dollars used: {dollars_used}", dollars_used=dollars_used)
-
- return CostedLanguageModelResponse(
- usage=LanguageModelResponseUsage(
- prompt_tokens_used=prompt_tokens,
- completion_tokens_used=completion_tokens,
- dollars_used=dollars_used,
- caching_info=caching_info,
- ),
- responses=tuple(results),
- )
-
- async def _get_api_stream(
- self,
- prompt: str,
- params: LanguageModelGenerationParams,
- ) -> AsyncGenerator[LanguageModelStreamEvent, None]:
- messages = convert_prompt_to_openai_messages(prompt)
-
- with self._exception_handler(prompt):
- client = self._get_client()
-
- temperature: NotGiven | float = params.temperature
-
- api_result = await client.chat.completions.create(
- model=self.model_name,
- messages=messages,
- max_completion_tokens=params.max_tokens,
- n=1,
- temperature=temperature,
- stop=params.stop,
- seed=params.seed,
- stream=True,
- stream_options={"include_usage": True},
- presence_penalty=self.presence_penalty,
- )
- assert isinstance(api_result, AsyncStream)
-
- yield LanguageModelStreamStartEvent()
-
- usage = None
- finish_reason: str | None = None
- async for chunk in api_result:
- if hasattr(chunk, "usage") and chunk.usage is not None:
- usage = chunk.usage
- continue
-
- if chunk.choices:
- assert len(chunk.choices) == 1, "Currently only count=1 supported for streaming API."
- data = only(chunk.choices)
- delta = data.delta.content
- if delta is not None:
- yield LanguageModelStreamDeltaEvent(delta=delta)
- if data.finish_reason:
- finish_reason = str(data.finish_reason)
-
- stop_reason = _OPENAI_COMPATIBLE_STOP_REASON_TO_STOP_REASON.get(str(finish_reason), ResponseStopReason.NONE)
- if params.stop is not None and stop_reason == ResponseStopReason.END_TURN:
- yield LanguageModelStreamDeltaEvent(delta=params.stop)
-
- if usage is not None:
- completion_tokens = usage.completion_tokens
- prompt_tokens = usage.prompt_tokens
- dollars_used = self.calculate_cost(prompt_tokens, completion_tokens)
- cached_tokens = (
- usage.prompt_tokens_details.cached_tokens if usage.prompt_tokens_details is not None else 0
- ) or 0
- caching_info = CachingInfo(
- read_from_cache=cached_tokens,
- provider_specific_data=OpenAICachingInfo(),
- )
- else:
- completion_tokens = -1
- prompt_tokens = -1
- dollars_used = -1
- caching_info = None
- logger.trace("dollars used: {dollars_used}", dollars_used=dollars_used)
-
- yield LanguageModelStreamEndEvent(
- usage=LanguageModelResponseUsage(
- prompt_tokens_used=prompt_tokens,
- completion_tokens_used=completion_tokens,
- dollars_used=dollars_used,
- caching_info=caching_info,
- ),
- stop_reason=stop_reason,
- )
-
- def count_tokens(self, text: str) -> int:
- return approximate_token_count(text)
-
- def _parse_response(
- self,
- response: ChatCompletion,
- prompt_tokens: int,
- stop: str | None,
- network_failure_count: int,
- ) -> tuple[LanguageModelResponse, ...]:
- results = []
- for data in response.choices:
- assert data.message.content is not None
- text = data.message.content
- token_count = self.count_tokens(text) + prompt_tokens
- stop_reason = _OPENAI_COMPATIBLE_STOP_REASON_TO_STOP_REASON.get(
- str(data.finish_reason), ResponseStopReason.NONE
- )
- if stop is not None and stop_reason == ResponseStopReason.END_TURN:
- text += stop
- result = LanguageModelResponse(
- text=text,
- token_count=token_count,
- stop_reason=stop_reason,
- network_failure_count=network_failure_count,
- )
- results.append(result)
- return tuple(results)
diff --git a/imbue_core/imbue_core/agents/llm_apis/openai_data_types.py b/imbue_core/imbue_core/agents/llm_apis/openai_data_types.py
@@ -1,13 +0,0 @@
-from imbue_core.pydantic_serialization import SerializableModel
-
-
-class OpenAIModelInfo(SerializableModel):
- """Currently there isn't any model info specific to OpenAI"""
-
- object_type: str = "OpenAIModelInfo"
-
-
-class OpenAICachingInfo(SerializableModel):
- """Currently there isn't any caching info specific to OpenAI"""
-
- object_type: str = "OpenAICachingInfo"
diff --git a/imbue_core/imbue_core/agents/llm_apis/stream.py b/imbue_core/imbue_core/agents/llm_apis/stream.py
@@ -1,168 +0,0 @@
-import abc
-import asyncio
-from collections.abc import AsyncIterator
-from contextlib import aclosing
-from typing import Any
-from typing import AsyncGenerator
-from typing import Sequence
-
-import anyio
-
-from imbue_core.agents.llm_apis.data_types import CachedCostedLanguageModelResponse
-from imbue_core.agents.llm_apis.data_types import CostedLanguageModelResponse
-from imbue_core.agents.llm_apis.data_types import LanguageModelResponse
-from imbue_core.agents.llm_apis.data_types import LanguageModelResponseUsage
-from imbue_core.agents.llm_apis.data_types import LanguageModelStreamInputs
-from imbue_core.agents.llm_apis.data_types import ModelResponse
-from imbue_core.agents.llm_apis.data_types import ResponseStopReason
-from imbue_core.agents.primitives.resource_limits import PaymentAuthorization
-from imbue_core.agents.primitives.resource_limits import get_global_resource_limits
-from imbue_core.caching import AsyncCache
-from imbue_core.pydantic_serialization import SerializableModel
-
-
-class LanguageModelStreamStartEvent(SerializableModel):
- pass
-
-
-class LanguageModelStreamDeltaEvent(SerializableModel):
- delta: str
- # TODO add per delta token count (if there is a demand)
- # TODO add per delta logprobs (if there is a demand)
-
-
-class LanguageModelStreamEndEvent(SerializableModel):
- usage: LanguageModelResponseUsage
- stop_reason: ResponseStopReason
-
-
-LanguageModelStreamEvent = LanguageModelStreamStartEvent | LanguageModelStreamDeltaEvent | LanguageModelStreamEndEvent
-
-
-class LanguageModelStreamCallback(abc.ABC, SerializableModel):
- @abc.abstractmethod
- async def __call__(self, response: CostedLanguageModelResponse) -> None: ...
-
-
-class UpdateCacheCallback(LanguageModelStreamCallback):
- key: str
- cache: AsyncCache[CachedCostedLanguageModelResponse]
- api_inputs: LanguageModelStreamInputs | None
-
- async def __call__(self, response: CostedLanguageModelResponse) -> None:
- async with self.cache:
- await self.cache.set(
- self.key,
- CachedCostedLanguageModelResponse(response=response, inputs=self.api_inputs),
- )
-
-
-class PromptDebuggingCallback(LanguageModelStreamCallback):
- prompt: str
- output_path: anyio.Path
-
- async def __call__(self, response: CostedLanguageModelResponse) -> None:
- await self.output_path.write_text(self.prompt + response.responses[0].text)
-
-
-class SettleSpendCallback(LanguageModelStreamCallback):
- auth: PaymentAuthorization
-
- async def __call__(self, response: CostedLanguageModelResponse) -> None:
- dollars_used = response.usage.dollars_used
- global_resource_limits = get_global_resource_limits()
- assert global_resource_limits is not None
- await global_resource_limits.settle_spend(self.auth, dollars_used)
- return None
-
-
-async def consume_async_iterator(iterator: AsyncIterator[Any]) -> None:
- async for _ in iterator:
- ...
-
-
-async def get_cached_response_stream(
- response: CostedLanguageModelResponse,
-) -> AsyncGenerator[LanguageModelStreamEvent, None]:
- """Simple stream that return cached response in a single go.
-
- Implemented here so user get's a consistent interface from stream().
- """
- yield LanguageModelStreamStartEvent()
- yield LanguageModelStreamDeltaEvent(delta=response.responses[0].text)
- yield LanguageModelStreamEndEvent(usage=response.usage, stop_reason=response.responses[0].stop_reason)
-
-
-class StreamedLanguageModelResponse(ModelResponse):
- """A stream of LanguageModel API events."""
-
- text_stream: AsyncIterator[str]
-
- def __init__(
- self,
- # Note event_stream is AsyncGenerator as it supports aclose for better cleanup (unlike AsyncIterator)
- event_stream: AsyncGenerator[LanguageModelStreamEvent, None],
- network_failure_count: int,
- completion_callbacks: Sequence[LanguageModelStreamCallback] = (),
- ) -> None:
- # the underlying stream coming from the API
- self._event_stream = event_stream
- self.text_stream = self._stream_text()
- self._final_message_snapshot: LanguageModelResponse | None = None
-
- self._completion_callbacks = completion_callbacks
- # this is propogated to final message
- self._network_failure_count = network_failure_count
- self.stop_reason: ResponseStopReason | None = None
-
- async def get_final_message(self) -> LanguageModelResponse:
- # wait until final message
- await consume_async_iterator(self._event_stream)
- assert self._final_message_snapshot is not None
- return self._final_message_snapshot
-
- async def __aiter__(self) -> AsyncGenerator[LanguageModelStreamEvent, None]:
- # iterator of events, with handling of shutdown
- async with aclosing(self._event_stream) as event_stream:
- deltas: list[str] = []
- async for event in event_stream:
- if isinstance(event, LanguageModelStreamStartEvent):
- # Need nested if statement here for outer if-elif-else to correctly filter for unknown event types
- if len(deltas) > 0 or self._final_message_snapshot is not None:
- raise RuntimeError("Start event should be the first event in stream.")
- elif isinstance(event, LanguageModelStreamDeltaEvent):
- deltas.append(event.delta)
- elif isinstance(event, LanguageModelStreamEndEvent):
- self.stop_reason = event.stop_reason
- self._final_message_snapshot = LanguageModelResponse(
- text="".join(deltas),
- token_count=(0 if event.usage is None else event.usage.completion_tokens_used),
- stop_reason=event.stop_reason,
- network_failure_count=self._network_failure_count,
- )
- if self._completion_callbacks is not None:
- costed_response = CostedLanguageModelResponse(
- usage=event.usage, responses=(self._final_message_snapshot,)
- )
- await asyncio.gather(*[callback(costed_response) for callback in self._completion_callbacks])
- else:
- raise ValueError(f"Unknown or Unexpected StreamEvent type {type(event)}.")
-
- yield event
-
- async def _stream_text(self) -> AsyncIterator[str]:
- # iterator of text delta
- async for event in self:
- if isinstance(event, LanguageModelStreamDeltaEvent):
- yield event.delta
-
- async def __aenter__(self) -> "StreamedLanguageModelResponse":
- return self
-
- async def __aexit__(self, *exc: Any) -> None:
- await self.aclose()
-
- async def aclose(self) -> None:
- """Close iterator."""
- # clean up underlying stream (currently not sure how to do this/if we need to do this)
- await self._event_stream.aclose()
diff --git a/imbue_core/imbue_core/agents/llm_apis/together_api.py b/imbue_core/imbue_core/agents/llm_apis/together_api.py
@@ -1,611 +0,0 @@
-import asyncio
-import enum
-import math
-from collections import defaultdict
-from contextlib import contextmanager
-from typing import AsyncGenerator
-from typing import Final
-from typing import Iterable
-from typing import Iterator
-from typing import Mapping
-from typing import cast
-
-from loguru import logger
-from pydantic.functional_validators import field_validator
-from together import AsyncTogether
-from together.abstract.api_requestor import APIRequestor
-from together.abstract.api_requestor import AioHTTPSession
-from together.error import APIConnectionError
-from together.error import APIError
-from together.error import AuthenticationError
-from together.error import InvalidRequestError
-from together.error import RateLimitError
-from together.error import ServiceUnavailableError
-from together.together_response import TogetherResponse
-from together.types import TogetherRequest
-from together.types.chat_completions import ChatCompletionResponse
-
-from imbue_core.agents.llm_apis.data_types import CostedLanguageModelResponse
-from imbue_core.agents.llm_apis.data_types import LanguageModelGenerationParams
-from imbue_core.agents.llm_apis.data_types import LanguageModelResponseUsage
-from imbue_core.agents.llm_apis.data_types import LanguageModelResponseWithLogits
-from imbue_core.agents.llm_apis.data_types import ResponseStopReason
-from imbue_core.agents.llm_apis.data_types import TokenProbability
-from imbue_core.agents.llm_apis.errors import BadAPIRequestError
-from imbue_core.agents.llm_apis.errors import LanguageModelInvalidModelNameError
-from imbue_core.agents.llm_apis.errors import MissingAPIKeyError
-from imbue_core.agents.llm_apis.errors import TransientLanguageModelError
-from imbue_core.agents.llm_apis.language_model_api import LanguageModelAPI
-from imbue_core.agents.llm_apis.models import ModelInfo
-from imbue_core.agents.llm_apis.stream import LanguageModelStreamDeltaEvent
-from imbue_core.agents.llm_apis.stream import LanguageModelStreamEndEvent
-from imbue_core.agents.llm_apis.stream import LanguageModelStreamEvent
-from imbue_core.agents.llm_apis.stream import LanguageModelStreamStartEvent
-from imbue_core.frozen_utils import FrozenDict
-from imbue_core.frozen_utils import FrozenMapping
-from imbue_core.itertools import only
-from imbue_core.secrets_utils import get_secret
-
-
-# This function is monkeypatched as the original method does not catch BaseExceptions and asyncio.CancelledErrors are BaseExceptions
-async def arequest(
- self: APIRequestor,
- options: TogetherRequest,
- stream: bool = False,
- request_timeout: float | tuple[float, float] | None = None,
-) -> tuple[TogetherResponse | AsyncGenerator[TogetherResponse, None], bool, str | None]:
- ctx = AioHTTPSession()
- session = await ctx.__aenter__()
- result = None
- try:
- result = await self.arequest_raw(
- options,
- session,
- request_timeout=request_timeout,
- )
- resp, got_stream = await self._interpret_async_response(result, stream)
- except BaseException:
- # Close the request before exiting session context.
- if result is not None:
- result.release()
- await ctx.__aexit__(None, None, None)
- raise
- if got_stream:
-
- async def wrap_resp() -> AsyncGenerator[TogetherResponse, None]:
- assert isinstance(resp, AsyncGenerator)
- try:
- async for r in resp:
- yield r
- finally:
- # Close the request before exiting session context. Important to do it here
- # as if stream is not fully exhausted, we need to close the request nevertheless.
- result.release()
- await ctx.__aexit__(None, None, None)
-
- return wrap_resp(), got_stream, self.api_key
- else:
- # Close the request before exiting session context.
- result.release()
- await ctx.__aexit__(None, None, None)
- return resp, got_stream, self.api_key
-
-
-APIRequestor.arequest = arequest # pyre-fixme[8]: pyre is confused about this
-
-
-class TogetherAIModelName(enum.StrEnum):
- GOOGLE_GEMMA_2_27B_IT = "together/google/gemma-2-27b-it"
- GOOGLE_GEMMA_2_9B_IT = "together/google/gemma-2-9b-it"
- GOOGLE_GEMMA_2B_IT = "together/google/gemma-2b-it"
- META_LLAMA_3_2_3B_INSTRUCT_TURBO = "together/meta-llama/Llama-3.2-3B-Instruct-Turbo"
- META_LLAMA_3_3_70B_INSTRUCT_TURBO = "together/meta-llama/Llama-3.3-70B-Instruct-Turbo"
- META_LLAMA_3_70B_CHAT_HF = "together/meta-llama/Llama-3-70b-chat-hf"
- META_LLAMA_3_8B_CHAT_HF = "together/meta-llama/Llama-3-8b-chat-hf"
- META_LLAMA_3_1_405B_INSTRUCT_TURBO = "together/meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo"
- META_LLAMA_3_1_70B_INSTRUCT_TURBO = "together/meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo"
- META_LLAMA_3_1_8B_INSTRUCT_TURBO = "together/meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo"
- META_LLAMA_3_70B_INSTRUCT_LITE = "together/meta-llama/Meta-Llama-3-70B-Instruct-Lite"
- META_LLAMA_3_70B_INSTRUCT_TURBO = "together/meta-llama/Meta-Llama-3-70B-Instruct-Turbo"
- META_LLAMA_3_8B_INSTRUCT_LITE = "together/meta-llama/Meta-Llama-3-8B-Instruct-Lite"
- META_LLAMA_3_8B_INSTRUCT_TURBO = "together/meta-llama/Meta-Llama-3-8B-Instruct-Turbo"
- MISTRALAI_MISTRAL_7B_INSTRUCT_V0_1 = "together/mistralai/Mistral-7B-Instruct-v0.1"
- MISTRALAI_MISTRAL_7B_INSTRUCT_V0_2 = "together/mistralai/Mistral-7B-Instruct-v0.2"
- MISTRALAI_MISTRAL_7B_INSTRUCT_V0_3 = "together/mistralai/Mistral-7B-Instruct-v0.3"
- MISTRALAI_MIXTRAL_8X22B_INSTRUCT_V0_1 = "together/mistralai/Mixtral-8x22B-Instruct-v0.1"
- MISTRALAI_MIXTRAL_8X7B_INSTRUCT_V0_1 = "together/mistralai/Mixtral-8x7B-Instruct-v0.1"
- NOUSRESEARCH_NOUS_HERMES_2_MIXTRAL_8X7B_DPO = "together/NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO"
- DEEPSEEK_R1 = "together/deepseek-ai/DeepSeek-R1"
- OPENAI_GPT_OSS_20B = "together/openai/gpt-oss-20b"
- OPENAI_GPT_OSS_120B = "together/openai/gpt-oss-120b"
- # TOGETHERCOMPUTER_LLAMA_3_8B_CHAT_HF_INT4 = "together/togethercomputer/Llama-3-8b-chat-hf-int4"
- # TOGETHERCOMPUTER_LLAMA_3_8B_CHAT_HF_INT8 = "together/togethercomputer/Llama-3-8b-chat-hf-int8"
-
-
-# Rate limits for Together AI models based on published API documentation
-# Reference: https://docs.together.ai/docs/rate-limits
-# Using Tier 5 rate limits (6,000 RPM)
-
-TOGETHERAI_MODEL_INFO_BY_NAME: FrozenMapping[TogetherAIModelName, ModelInfo] = FrozenDict(
- {
- # ref https://docs.together.ai/docs/chat-models
- # pricing ref https://www.together.ai/pricing
- TogetherAIModelName.GOOGLE_GEMMA_2_27B_IT: ModelInfo(
- model_name=str(TogetherAIModelName.GOOGLE_GEMMA_2_27B_IT),
- cost_per_input_token=0.8 / 1_000_000,
- cost_per_output_token=0.8 / 1_000_000,
- max_input_tokens=8192,
- max_output_tokens=None,
- rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
- ),
- TogetherAIModelName.GOOGLE_GEMMA_2_9B_IT: ModelInfo(
- model_name=str(TogetherAIModelName.GOOGLE_GEMMA_2_9B_IT),
- cost_per_input_token=0.3 / 1_000_000,
- cost_per_output_token=0.3 / 1_000_000,
- max_input_tokens=8192,
- max_output_tokens=None,
- rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
- ),
- TogetherAIModelName.GOOGLE_GEMMA_2B_IT: ModelInfo(
- model_name=str(TogetherAIModelName.GOOGLE_GEMMA_2B_IT),
- cost_per_input_token=0.1 / 1_000_000,
- cost_per_output_token=0.1 / 1_000_000,
- max_input_tokens=8192,
- max_output_tokens=None,
- rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
- ),
- TogetherAIModelName.META_LLAMA_3_2_3B_INSTRUCT_TURBO: ModelInfo(
- model_name=str(TogetherAIModelName.META_LLAMA_3_2_3B_INSTRUCT_TURBO),
- cost_per_input_token=0.06 / 1_000_000,
- cost_per_output_token=0.06 / 1_000_000,
- max_input_tokens=131072,
- max_output_tokens=None,
- rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
- ),
- TogetherAIModelName.META_LLAMA_3_3_70B_INSTRUCT_TURBO: ModelInfo(
- model_name=str(TogetherAIModelName.META_LLAMA_3_3_70B_INSTRUCT_TURBO),
- cost_per_input_token=0.88 / 1_000_000,
- cost_per_output_token=0.88 / 1_000_000,
- max_input_tokens=131072,
- max_output_tokens=None,
- rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
- ),
- TogetherAIModelName.META_LLAMA_3_70B_CHAT_HF: ModelInfo(
- model_name=str(TogetherAIModelName.META_LLAMA_3_70B_CHAT_HF),
- cost_per_input_token=0.88 / 1_000_000,
- cost_per_output_token=0.88 / 1_000_000,
- max_input_tokens=8192,
- max_output_tokens=None,
- rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
- ),
- TogetherAIModelName.META_LLAMA_3_8B_CHAT_HF: ModelInfo(
- model_name=str(TogetherAIModelName.META_LLAMA_3_8B_CHAT_HF),
- cost_per_input_token=0.2 / 1_000_000,
- cost_per_output_token=0.2 / 1_000_000,
- max_input_tokens=8192,
- max_output_tokens=None,
- rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
- ),
- TogetherAIModelName.META_LLAMA_3_1_405B_INSTRUCT_TURBO: ModelInfo(
- model_name=str(TogetherAIModelName.META_LLAMA_3_1_405B_INSTRUCT_TURBO),
- cost_per_input_token=3.5 / 1_000_000,
- cost_per_output_token=3.5 / 1_000_000,
- max_input_tokens=130815,
- max_output_tokens=None,
- rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
- ),
- TogetherAIModelName.META_LLAMA_3_1_70B_INSTRUCT_TURBO: ModelInfo(
- model_name=str(TogetherAIModelName.META_LLAMA_3_1_70B_INSTRUCT_TURBO),
- cost_per_input_token=0.88 / 1_000_000,
- cost_per_output_token=0.88 / 1_000_000,
- max_input_tokens=131072,
- max_output_tokens=None,
- rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
- ),
- TogetherAIModelName.META_LLAMA_3_1_8B_INSTRUCT_TURBO: ModelInfo(
- model_name=str(TogetherAIModelName.META_LLAMA_3_1_8B_INSTRUCT_TURBO),
- cost_per_input_token=0.18 / 1_000_000,
- cost_per_output_token=0.18 / 1_000_000,
- max_input_tokens=131072,
- max_output_tokens=None,
- rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
- ),
- TogetherAIModelName.META_LLAMA_3_70B_INSTRUCT_LITE: ModelInfo(
- model_name=str(TogetherAIModelName.META_LLAMA_3_70B_INSTRUCT_LITE),
- cost_per_input_token=0.54 / 1_000_000,
- cost_per_output_token=0.54 / 1_000_000,
- max_input_tokens=8192,
- max_output_tokens=None,
- rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
- ),
- TogetherAIModelName.META_LLAMA_3_70B_INSTRUCT_TURBO: ModelInfo(
- model_name=str(TogetherAIModelName.META_LLAMA_3_70B_INSTRUCT_TURBO),
- cost_per_input_token=0.88 / 1_000_000,
- cost_per_output_token=0.88 / 1_000_000,
- max_input_tokens=8192,
- max_output_tokens=None,
- rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
- ),
- TogetherAIModelName.META_LLAMA_3_8B_INSTRUCT_LITE: ModelInfo(
- model_name=str(TogetherAIModelName.META_LLAMA_3_8B_INSTRUCT_LITE),
- cost_per_input_token=0.1 / 1_000_000,
- cost_per_output_token=0.1 / 1_000_000,
- max_input_tokens=8192,
- max_output_tokens=None,
- rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
- ),
- TogetherAIModelName.META_LLAMA_3_8B_INSTRUCT_TURBO: ModelInfo(
- model_name=str(TogetherAIModelName.META_LLAMA_3_8B_INSTRUCT_TURBO),
- cost_per_input_token=0.18 / 1_000_000,
- cost_per_output_token=0.18 / 1_000_000,
- max_input_tokens=8192,
- max_output_tokens=None,
- rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
- ),
- TogetherAIModelName.MISTRALAI_MISTRAL_7B_INSTRUCT_V0_1: ModelInfo(
- model_name=str(TogetherAIModelName.MISTRALAI_MISTRAL_7B_INSTRUCT_V0_1),
- cost_per_input_token=0.2 / 1_000_000,
- cost_per_output_token=0.2 / 1_000_000,
- max_input_tokens=32768,
- max_output_tokens=None,
- rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
- ),
- TogetherAIModelName.MISTRALAI_MISTRAL_7B_INSTRUCT_V0_2: ModelInfo(
- model_name=str(TogetherAIModelName.MISTRALAI_MISTRAL_7B_INSTRUCT_V0_2),
- cost_per_input_token=0.2 / 1_000_000,
- cost_per_output_token=0.2 / 1_000_000,
- max_input_tokens=32768,
- max_output_tokens=None,
- rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
- ),
- TogetherAIModelName.MISTRALAI_MISTRAL_7B_INSTRUCT_V0_3: ModelInfo(
- model_name=str(TogetherAIModelName.MISTRALAI_MISTRAL_7B_INSTRUCT_V0_3),
- cost_per_input_token=0.2 / 1_000_000,
- cost_per_output_token=0.2 / 1_000_000,
- max_input_tokens=32768,
- max_output_tokens=None,
- rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
- ),
- TogetherAIModelName.MISTRALAI_MIXTRAL_8X22B_INSTRUCT_V0_1: ModelInfo(
- model_name=str(TogetherAIModelName.MISTRALAI_MIXTRAL_8X22B_INSTRUCT_V0_1),
- cost_per_input_token=1.2 / 1_000_000,
- cost_per_output_token=1.2 / 1_000_000,
- max_input_tokens=65536,
- max_output_tokens=None,
- rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
- ),
- TogetherAIModelName.MISTRALAI_MIXTRAL_8X7B_INSTRUCT_V0_1: ModelInfo(
- model_name=str(TogetherAIModelName.MISTRALAI_MIXTRAL_8X7B_INSTRUCT_V0_1),
- cost_per_input_token=0.6 / 1_000_000,
- cost_per_output_token=0.6 / 1_000_000,
- max_input_tokens=32768,
- max_output_tokens=None,
- rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
- ),
- TogetherAIModelName.NOUSRESEARCH_NOUS_HERMES_2_MIXTRAL_8X7B_DPO: ModelInfo(
- model_name=str(TogetherAIModelName.NOUSRESEARCH_NOUS_HERMES_2_MIXTRAL_8X7B_DPO),
- cost_per_input_token=0.6 / 1_000_000,
- cost_per_output_token=0.6 / 1_000_000,
- max_input_tokens=32768,
- max_output_tokens=None,
- rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
- ),
- TogetherAIModelName.DEEPSEEK_R1: ModelInfo(
- model_name=str(TogetherAIModelName.DEEPSEEK_R1),
- cost_per_input_token=3.0 / 1_000_000,
- cost_per_output_token=7.0 / 1_000_000,
- max_input_tokens=32768,
- max_output_tokens=None,
- rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
- ),
- TogetherAIModelName.OPENAI_GPT_OSS_20B: ModelInfo(
- model_name=str(TogetherAIModelName.OPENAI_GPT_OSS_20B),
- cost_per_input_token=0.00 / 1_000_000,
- cost_per_output_token=0.00 / 1_000_000,
- max_input_tokens=131_072,
- max_output_tokens=131_072,
- rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
- ),
- TogetherAIModelName.OPENAI_GPT_OSS_120B: ModelInfo(
- model_name=str(TogetherAIModelName.OPENAI_GPT_OSS_120B),
- cost_per_input_token=0.00 / 1_000_000,
- cost_per_output_token=0.00 / 1_000_000,
- max_input_tokens=131_072,
- max_output_tokens=131_072,
- rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
- ),
- }
-)
-
-
-def _default_capacity_semaphor() -> asyncio.Semaphore:
- return asyncio.Semaphore(100)
-
-
-_CAPACITY_SEMAPHOR_BY_MODEL_NAME: Mapping[str, asyncio.Semaphore] = defaultdict(_default_capacity_semaphor)
-
-
-_ROLE_TO_TOGETHERAI_ROLE: Final[FrozenMapping] = FrozenDict(
- {
- "HUMAN": "user",
- "ASSISTANT": "assistant",
- "SYSTEM": "system",
- "SYSTEM_CACHED": "system",
- "USER": "user",
- "USER_CACHED": "user",
- }
-)
-
-# ref: https://github.com/togethercomputer/together-python/blob/main/src/together/types/common.py#L13
-_TOGETHERAI_STOP_REASON_TO_STOP_REASON: Final[FrozenMapping[str, ResponseStopReason]] = FrozenDict(
- {
- "length": ResponseStopReason.MAX_TOKENS,
- # This is a little sketchy, we treat them the same since we don't know which models emit stop sequence reasons
- # Since we don't want to break downstream applications that may require ending in the stop sequence
- # This is similar to how openai models are treated
- "stop": ResponseStopReason.END_TURN,
- "eos": ResponseStopReason.END_TURN,
- "tool_calls": ResponseStopReason.TOOL_CALLS,
- "error": ResponseStopReason.ERROR,
- "None": ResponseStopReason.NONE,
- }
-)
-
-
-def convert_prompt_to_together_messages(prompt: str) -> list[dict[str, str]]:
- prompt = prompt.lstrip()
- assert prompt.endswith("\n[ROLE=ASSISTANT]\n"), "prompt must end with [ROLE=ASSISTANT], prompt=\n" + prompt
- prompt = "".join(prompt.rsplit("\n[ROLE=ASSISTANT]\n", 1))
- assert prompt.startswith("[ROLE=")
- prompt = prompt.replace("[ROLE=", "", 1)
- chunks = prompt.split("\n[ROLE=")
- messages: list[dict[str, str]] = []
- for chunk in chunks:
- lines = chunk.split("\n")
- role = lines[0].strip().rstrip("]")
- assert role in (
- "HUMAN",
- "ASSISTANT",
- "USER",
- "SYSTEM",
- "SYSTEM_CACHED",
- "USER_CACHED",
- ), f"Unknown role {role} in prompt {prompt}"
- lines.pop(0)
- if len(messages) > 0:
- messages[-1]["content"] = messages[-1]["content"] + "\n"
- messages.append({"role": _ROLE_TO_TOGETHERAI_ROLE[role], "content": "\n".join(lines)})
- return messages
-
-
-@contextmanager
-def _together_exception_manager() -> Iterator[None]:
- """Simple context manager for parsing TogetherAI API exceptions."""
- # ref https://github.com/togethercomputer/together-python/blob/main/src/together/abstract/api_requestor.py#L332
- try:
- yield
- except RateLimitError as e:
- logger.info("Rate limited? {}", e)
- raise TransientLanguageModelError("RateLimitError") from e
- except InvalidRequestError as e:
- logger.info("BadAPIRequestError {}", e)
- raise BadAPIRequestError(str(e)) from e
- except AuthenticationError as e:
- logger.info("API Authentication error {}", e)
- raise
- except APIError as e:
- logger.info("Received APIError {}", e)
- raise
- except ServiceUnavailableError as e:
- logger.info("Received ServiceUnavailableError {}", e)
- raise TransientLanguageModelError("ServiceUnavailableError") from e
- except APIConnectionError as e:
- logger.info("Received APIConnectionError {}", e)
- raise TransientLanguageModelError("APIConnectionError") from e
- # Note, the together python SDK uses aiohttp under the hood
- # but takes care of parsing the main aiohttp exceptions into together API exceptions
- # ref https://github.com/togethercomputer/together-python/blob/main/src/together/abstract/api_requestor.py#L554
-
-
-class TogetherAPI(LanguageModelAPI):
- model_name: TogetherAIModelName = TogetherAIModelName.META_LLAMA_3_1_8B_INSTRUCT_TURBO
- is_conversational: bool = True
- presence_penalty: float = 0.0
- # this shouldn't really ever even be used, but just in case
- stop_token_log_probability: float = math.log(0.9999)
-
- @field_validator("model_name") # pyre-ignore[56]: pyre doesn't understand pydantic
- @classmethod
- def validate_model_name(cls, v: str) -> str:
- if v not in TOGETHERAI_MODEL_INFO_BY_NAME:
- raise LanguageModelInvalidModelNameError(v, cls.__name__, list(TOGETHERAI_MODEL_INFO_BY_NAME))
- return v
-
- @property
- def model_info(self) -> ModelInfo:
- return TOGETHERAI_MODEL_INFO_BY_NAME[self.model_name]
-
- @property
- def external_model_name(self) -> str:
- return self.model_name.replace("together/", "")
-
- def _get_client(self) -> AsyncTogether:
- api_key = get_secret("TOGETHER_API_KEY")
- if not api_key:
- raise MissingAPIKeyError("TOGETHER_API_KEY environment variable is not set")
- return AsyncTogether(api_key=api_key)
-
- async def _call_api(
- self,
- prompt: str,
- params: LanguageModelGenerationParams,
- network_failure_count: int = 0,
- ) -> CostedLanguageModelResponse:
- if params.max_tokens is None:
- logger.debug(
- "Togetherai API breaks if `max_tokens` not specified. Defaulting to `max_tokens=512`, make sure to specify this if you want something different."
- )
- params.max_tokens = 512
-
- with _together_exception_manager():
- messages = convert_prompt_to_together_messages(prompt)
- client = self._get_client()
- async with _CAPACITY_SEMAPHOR_BY_MODEL_NAME[self.model_name]:
- # ref: https://github.com/togethercomputer/together-python/blob/main/src/together/resources/chat/completions.py#L153
- api_result = await client.chat.completions.create(
- model=self.external_model_name,
- messages=messages,
- max_tokens=params.max_tokens,
- stop=[params.stop] if params.stop else None,
- temperature=params.temperature,
- top_k=5,
- presence_penalty=self.presence_penalty,
- stream=False,
- logprobs=True,
- n=params.count,
- # currently don't specify this since tokenizer may change between models
- logit_bias=None,
- )
- assert isinstance(api_result, ChatCompletionResponse)
-
- results = []
- choices = api_result.choices
- assert choices is not None
- for data in choices:
- message = data.message
- assert message is not None
- text = message.content
- assert text is not None
- assert isinstance(text, str) # TODO: this is suspicious
-
- # TogetherAI only provides the logprob for the selected token
- logprobs = data.logprobs
- assert logprobs is not None
- tokens = logprobs.tokens
- token_logprobs = logprobs.token_logprobs
- assert tokens is not None and token_logprobs is not None
- assert all(token is not None for token in tokens)
- assert all(logprob is not None for logprob in token_logprobs)
- tokens = cast(Iterable[str], tokens)
- token_logprobs = cast(Iterable[float], token_logprobs)
- token_probabilities = [
- (TokenProbability(token=token, log_probability=logprob, is_stop=False),)
- for token, logprob in zip(tokens, token_logprobs)
- ]
-
- if data.finish_reason:
- stop_reason = _TOGETHERAI_STOP_REASON_TO_STOP_REASON[data.finish_reason.value]
- else:
- stop_reason = ResponseStopReason.NONE
- stop = params.stop
- if stop is not None and stop_reason == ResponseStopReason.END_TURN:
- text += stop
- token_probabilities.append(
- (
- TokenProbability(
- token=stop,
- log_probability=self.stop_token_log_probability,
- is_stop=True,
- ),
- )
- )
- result = LanguageModelResponseWithLogits(
- text=text,
- token_probabilities=tuple(token_probabilities),
- token_count=len(token_probabilities),
- stop_reason=stop_reason,
- network_failure_count=network_failure_count,
- )
- results.append(result)
-
- logger.trace("text: " + results[0].text)
- if api_result.usage is not None:
- completion_tokens = api_result.usage.completion_tokens
- prompt_tokens = api_result.usage.prompt_tokens
- else:
- completion_tokens = 0
- prompt_tokens = 0
- dollars_used = self.calculate_cost(prompt_tokens, completion_tokens)
- logger.trace("dollars used: {}", dollars_used)
- return CostedLanguageModelResponse(
- usage=LanguageModelResponseUsage(
- prompt_tokens_used=prompt_tokens,
- completion_tokens_used=completion_tokens,
- dollars_used=dollars_used,
- ),
- responses=tuple(results),
- )
-
- async def _get_api_stream(
- self,
- prompt: str,
- params: LanguageModelGenerationParams,
- ) -> AsyncGenerator[LanguageModelStreamEvent, None]:
- if params.max_tokens is None:
- logger.debug(
- "Togetherai API breaks if `max_tokens` not specified. Defaulting to `max_tokens=512`, make sure to specify this if you want something different."
- )
- params.max_tokens = 512
-
- with _together_exception_manager():
- messages = convert_prompt_to_together_messages(prompt)
- client = self._get_client()
- async with _CAPACITY_SEMAPHOR_BY_MODEL_NAME[self.model_name]:
- # ref: https://github.com/togethercomputer/together-python/blob/main/src/together/resources/chat/completions.py#L153
- api_result = await client.chat.completions.create(
- model=self.external_model_name,
- messages=messages,
- max_tokens=params.max_tokens,
- stop=[params.stop] if params.stop else None,
- temperature=params.temperature,
- top_k=5,
- presence_penalty=self.presence_penalty,
- stream=True,
- # currently we don't support logprobs with streaming
- logprobs=False,
- n=1,
- # currently don't specify this since tokenizer may change between models
- logit_bias=None,
- )
- assert isinstance(api_result, AsyncGenerator)
-
- yield LanguageModelStreamStartEvent()
-
- usage = None
- finish_reason: str | None = None
- async for chunk in api_result:
- if chunk.usage:
- usage = chunk.usage
-
- if chunk.finish_reason:
- finish_reason = chunk.finish_reason.value
-
- chunk_choices = chunk.choices
- if chunk_choices:
- assert len(chunk_choices) == 1, "Currently only count=1 supported for streaming API."
- delta = only(chunk_choices).delta
- if delta and delta.content:
- yield LanguageModelStreamDeltaEvent(delta=delta.content)
-
- stop_reason = _TOGETHERAI_STOP_REASON_TO_STOP_REASON[str(finish_reason)]
-
- if params.stop is not None and stop_reason == ResponseStopReason.END_TURN:
- yield LanguageModelStreamDeltaEvent(delta=params.stop)
-
- if usage is not None:
- completion_tokens = usage.completion_tokens
- prompt_tokens = usage.prompt_tokens
- else:
- completion_tokens = 0
- prompt_tokens = 0
- dollars_used = self.calculate_cost(prompt_tokens, completion_tokens)
- logger.trace("dollars used: {}", dollars_used)
-
- yield LanguageModelStreamEndEvent(
- usage=LanguageModelResponseUsage(
- prompt_tokens_used=prompt_tokens,
- completion_tokens_used=completion_tokens,
- dollars_used=dollars_used,
- ),
- stop_reason=stop_reason,
- )
diff --git a/imbue_core/imbue_core/agents/llm_apis/union_data_types.py b/imbue_core/imbue_core/agents/llm_apis/union_data_types.py
@@ -1,20 +0,0 @@
-from typing import Annotated
-
-from pydantic import Tag
-
-from imbue_core.agents.llm_apis.anthropic_data_types import AnthropicCachingInfo
-from imbue_core.agents.llm_apis.anthropic_data_types import AnthropicModelInfo
-from imbue_core.agents.llm_apis.openai_data_types import OpenAICachingInfo
-from imbue_core.agents.llm_apis.openai_data_types import OpenAIModelInfo
-from imbue_core.pydantic_serialization import build_discriminator
-
-ProviderSpecificModelInfoUnion = Annotated[
- Annotated[AnthropicModelInfo, Tag("AnthropicModelInfo")] | Annotated[OpenAIModelInfo, Tag("OpenAIModelInfo")],
- build_discriminator(),
-]
-
-ProviderSpecificCachingInfoUnion = Annotated[
- Annotated[AnthropicCachingInfo, Tag("AnthropicCachingInfo")]
- | Annotated[OpenAICachingInfo, Tag("OpenAICachingInfo")],
- build_discriminator(),
-]
diff --git a/imbue_core/imbue_core/agents/primitives/resource_limits.py b/imbue_core/imbue_core/agents/primitives/resource_limits.py
@@ -1,485 +0,0 @@
-import asyncio
-import datetime
-import os
-from asyncio import CancelledError
-from asyncio import Task
-from asyncio import TaskGroup
-from typing import Any
-from typing import Callable
-from typing import Coroutine
-from typing import Optional
-from uuid import uuid4
-
-import attr
-from loguru import logger
-
-from imbue_core.agents.primitives.errors import DollarLimitExceeded
-from imbue_core.agents.primitives.errors import MaximumSpendExceeded
-from imbue_core.async_monkey_patches import safe_cancel
-from imbue_core.itertools import first
-from imbue_core.serialization_types import Serializable
-from imbue_core.time_utils import get_current_time
-
-# TODO: someday in the future we can be smarter about this...
-_AUTH_PAYMENT_TIMEOUT_SECONDS = 60 * 60 * 24
-_ONE_HOUR_IN_SECONDS = 60 * 60
-
-
-class InvalidResourceLimitsError(Exception):
- pass
-
-
-class AuthorizationInvalidated(Exception):
- pass
-
-
-@attr.s(auto_attribs=True, frozen=True)
-class PaymentAuthorization:
- dollars: float
- authorization_id: str
- authorized_at: datetime.datetime
-
-
-@attr.s(auto_attribs=True, frozen=True)
-class ResourceLimitState(Serializable):
- hard_cap_dollars: float
- hard_cap_seconds: float
- warn_cap_dollars: float
- warn_cap_seconds: float
- dollars_per_hour: float | None
-
- @classmethod
- def build_for_increase(
- cls,
- hard_cap_dollars: float = 0.001,
- hard_cap_seconds: float = 0.001,
- warn_cap_dollars: float = 0.001,
- warn_cap_seconds: float = 0.001,
- dollars_per_hour: float | None = None,
- ) -> "ResourceLimitState":
- return cls(
- hard_cap_dollars=hard_cap_dollars,
- hard_cap_seconds=hard_cap_seconds,
- warn_cap_dollars=warn_cap_dollars,
- warn_cap_seconds=warn_cap_seconds,
- dollars_per_hour=dollars_per_hour,
- )
-
-
-def _float_or_none(value: str | None) -> float | None:
- if value is None:
- return None
- return float(value)
-
-
-@attr.s(auto_attribs=True)
-class ResourceLimits:
- hard_cap_dollars: float
- hard_cap_seconds: float
- warn_cap_dollars: float
- warn_cap_seconds: float
- # note that setting this effectively caps the size of any given authorization request to this quantity as well
- # (since it is impossible to spend less than $X per hour if you are spending $X+1 in total)
- # in such a case, MaximumSpendExceeded will be raised
- dollars_per_hour: float | None = None
- parent_limits: Optional["ResourceLimits"] = None
- dollars_spent: float = 0.0
- save_spend_callback: Callable[[float], Coroutine[Any, Any, None]] | None = None
- open_authorizations: dict[str, PaymentAuthorization] = attr.ib(factory=dict)
- recent_spend_events: list[PaymentAuthorization] = attr.ib(factory=list)
- # ensure that only a single spend is being authorized at once
- spend_lock: asyncio.Lock = attr.ib(factory=asyncio.Lock)
- # prevent us from mutating our state from multiple coroutines at once
- state_lock: asyncio.Lock = attr.ib(factory=asyncio.Lock)
- # triggered when the limits are updated
- limits_updated_event: asyncio.Event = attr.ib(factory=asyncio.Event)
- # triggered when a settlement happens
- next_settlement_event: asyncio.Event = attr.ib(factory=asyncio.Event)
-
- @classmethod
- def build(
- cls,
- *,
- max_dollars: float | None = None,
- max_seconds: float | None = None,
- warn_fraction: float | None = None,
- dollars_per_hour: float | None = None,
- ) -> "ResourceLimits":
- if max_dollars is None and "DEFAULT_MAX_HAMMER_DOLLARS" in os.environ:
- max_dollars = _float_or_none(os.getenv("DEFAULT_MAX_HAMMER_DOLLARS"))
- assert max_dollars != float("inf"), "max_dollars must be finite"
- if max_dollars is None:
- max_dollars = 0.0
- assert max_dollars >= 0, "max_dollars must be non-negative"
- if max_seconds is None and "DEFAULT_MAX_HAMMER_SECONDS" in os.environ:
- max_seconds = _float_or_none(os.getenv("DEFAULT_MAX_HAMMER_SECONDS"))
- if max_seconds is None:
- max_seconds = float("inf")
- assert max_seconds >= 0, "max_seconds must be non-negative"
- if warn_fraction is None:
- # TODO: is it DEFAULT_WARN_FRACTION or DEFAULT_HAMMER_WARN_FRACTION?
- if "DEFAULT_WARN_FRACTION" in os.environ:
- warn_fraction = _float_or_none(os.getenv("DEFAULT_HAMMER_WARN_FRACTION"))
- assert warn_fraction is not None
- else:
- warn_fraction = 0.25
- if dollars_per_hour is None and "DEFAULT_DOLLARS_PER_HOUR" in os.environ:
- dollars_per_hour = _float_or_none(os.getenv("DEFAULT_DOLLARS_PER_HOUR"))
- result = cls(
- hard_cap_dollars=max_dollars,
- hard_cap_seconds=max_seconds,
- warn_cap_dollars=max_dollars * warn_fraction,
- warn_cap_seconds=max_seconds * warn_fraction,
- dollars_per_hour=dollars_per_hour,
- )
- # check that we're not currently in a hammer, otherwise should be calling create_restricted_limits instead
- try:
- if get_global_resource_limits() is not None:
- # If you encounter this, it probably means you're trying to mix hammer and non-hammer resource limiting.
- # For simplicity and correctness, try to avoid that.
- raise InvalidResourceLimitsError(
- "Should not create a new ResourceLimits while global limits are in place. Instead, call create_restricted_limits"
- )
- except RuntimeError:
- pass
- return result
-
- def create_restricted_limits(
- self,
- *,
- max_dollars: float | None = None,
- max_seconds: float | None = None,
- warn_fraction: float | None = None,
- hard_cap_dollars: float | None = None,
- hard_cap_seconds: float | None = None,
- warn_cap_dollars: float | None = None,
- warn_cap_seconds: float | None = None,
- dollars_per_hour: float | None = None,
- ) -> "ResourceLimits":
- if max_dollars is not None:
- assert hard_cap_dollars is None, "Cannot specify both max_dollars and hard_cap_dollars"
- hard_cap_dollars = max_dollars
- if warn_fraction is not None:
- assert warn_cap_dollars is None, "Cannot specify both warn_fraction and warn_cap_dollars"
- warn_cap_dollars = max_dollars * warn_fraction
-
- if max_seconds is not None:
- assert hard_cap_seconds is None, "Cannot specify both max_seconds and hard_cap_seconds"
- hard_cap_seconds = max_seconds
- if warn_fraction is not None:
- assert warn_cap_seconds is None, "Cannot specify both warn_fraction and warn_cap_seconds"
- warn_cap_seconds = max_seconds * warn_fraction
-
- if hard_cap_dollars is not None:
- hard_cap_dollars = min(hard_cap_dollars, self.hard_cap_dollars)
- if hard_cap_seconds is not None:
- hard_cap_seconds = min(hard_cap_seconds, self.hard_cap_seconds)
- if warn_cap_dollars is not None:
- warn_cap_dollars = min(warn_cap_dollars, self.warn_cap_dollars)
- if warn_cap_seconds is not None:
- warn_cap_seconds = min(warn_cap_seconds, self.warn_cap_seconds)
- if dollars_per_hour is not None and self.dollars_per_hour is not None:
- dollars_per_hour = min(dollars_per_hour, self.dollars_per_hour)
- return ResourceLimits(
- hard_cap_dollars=hard_cap_dollars or self.hard_cap_dollars,
- hard_cap_seconds=hard_cap_seconds or self.hard_cap_seconds,
- warn_cap_dollars=warn_cap_dollars or self.warn_cap_dollars,
- warn_cap_seconds=warn_cap_seconds or self.warn_cap_seconds,
- dollars_per_hour=dollars_per_hour or self.dollars_per_hour,
- parent_limits=self,
- )
-
- def _get_excessive_spend_message(self, dollars: float, debug_info: Any = None) -> str:
- msg = f"Tried to spend {dollars} but only {self.hard_cap_dollars - self.dollars_spent} left (of {self.hard_cap_dollars})."
- if self.hard_cap_dollars == 0:
- msg += " You might want to set DEFAULT_MAX_HAMMER_DOLLARS to something non-zero"
- if debug_info is not None:
- msg += f"\nDebug info: {debug_info}"
- return msg
-
- async def authorize_spend(self, dollars: float, debug_info: Any | None = None) -> PaymentAuthorization:
- # note that we purposefully lock here, even though we are potentially waiting inside the loop below.
- # the reason for this is that otherwise a large transaction could be starved by a series of smaller transactions
- # this is annoying to reason about, so this makes it FIFO instead (though potentially at the cost of having to
- # wait for a while if you are near the limit and there are smaller transactions that could have made it through)
- async with self.spend_lock:
- await self._clear_old_authorizations()
-
- if self.dollars_spent + dollars > self.hard_cap_dollars:
- raise DollarLimitExceeded(self._get_excessive_spend_message(dollars, debug_info))
-
- # if we have outstanding authorizations that mean that this transaction would put us over the limit, wait
- while (await self.get_dollars_authorized_and_spent()) + dollars > self.hard_cap_dollars and (
- await self.get_dollars_currently_authorized()
- ) > 0:
- await self.next_settlement_event.wait()
-
- # now that some authorizations have settled and we're done waiting, have to check again if this would put us over
- if self.dollars_spent + dollars > self.hard_cap_dollars:
- raise DollarLimitExceeded(self._get_excessive_spend_message(dollars))
-
- dollars_per_hour = self.dollars_per_hour
- if dollars_per_hour is not None:
- if dollars > dollars_per_hour:
- raise MaximumSpendExceeded(
- f"Tried to spend ${dollars} but only ${dollars_per_hour} / hr allowed, which caps the total spend"
- )
-
- # wait until the spend rate is low enough
- while (await self.get_dollars_authorized_and_spent_in_the_last_hour()) + dollars > dollars_per_hour:
- oldest_event = first(
- sorted(
- [x for x in self.recent_spend_events],
- key=lambda x: x.authorized_at,
- )
- )
- if oldest_event is None:
- break
- time_since_oldest_event = (get_current_time() - oldest_event.authorized_at).total_seconds()
- time_until_next_event_expires = _ONE_HOUR_IN_SECONDS - time_since_oldest_event
- logger.debug(
- f"Waiting until spend rate has subsided (currently at {(await self.get_dollars_authorized_and_spent_in_the_last_hour())} / hr)"
- )
- waiting_task = asyncio.create_task(self._wait_until_updated())
- try:
- await asyncio.wait_for(waiting_task, timeout=time_until_next_event_expires + 0.01)
- except TimeoutError:
- pass
- await self._clear_old_authorizations()
-
- assert (await self.get_dollars_authorized_and_spent_in_the_last_hour()) + dollars <= dollars_per_hour
-
- async with self.state_lock:
- if self.parent_limits is None:
- auth = PaymentAuthorization(
- dollars=dollars,
- authorization_id=uuid4().hex,
- authorized_at=get_current_time(),
- )
- else:
- auth = await self.parent_limits.authorize_spend(dollars)
- self.open_authorizations[auth.authorization_id] = auth
- return auth
-
- async def settle_spend(self, authorization: PaymentAuthorization, dollars: float) -> None:
- async with self.state_lock:
- await self._clear_old_authorizations(_is_already_locked=True)
-
- if self.parent_limits is not None:
- await self.parent_limits.settle_spend(authorization, dollars)
-
- is_threshold_exceeded_by_this_transaction = (
- self.dollars_spent < self.warn_cap_dollars <= self.dollars_spent + dollars
- )
-
- if authorization.authorization_id not in self.open_authorizations:
- raise AuthorizationInvalidated(
- f"Authorization {authorization.authorization_id} has timed out or already been settled"
- )
- del self.open_authorizations[authorization.authorization_id]
- self.recent_spend_events.append(authorization)
- self.dollars_spent += dollars
- assert self.save_spend_callback is not None, "Should have been initialized by now"
- await self.save_spend_callback(self.dollars_spent)
-
- # notify anything waiting on the next settlement
- self.next_settlement_event.set()
- self.next_settlement_event.clear()
-
- logger.trace(
- "Settled spend of {}, remaining: {}",
- dollars,
- self.hard_cap_dollars - self.dollars_spent,
- )
-
- if is_threshold_exceeded_by_this_transaction:
- await self._warn(f"Spent ${self.dollars_spent} already (will be stopped at ${self.hard_cap_dollars})")
-
- # TODO: make a more configurable warning system, right now just logs
- async def _warn(self, message: str) -> None:
- logger.warning(message)
-
- async def _clear_old_authorizations(self, _is_already_locked: bool = False) -> None:
- if not _is_already_locked:
- await self.state_lock.acquire()
- try:
- now = get_current_time()
- self.open_authorizations = {
- k: v
- for k, v in self.open_authorizations.items()
- if (now - v.authorized_at).total_seconds() < _AUTH_PAYMENT_TIMEOUT_SECONDS
- }
- self.recent_spend_events = [
- x for x in self.recent_spend_events if (now - x.authorized_at).total_seconds() < _ONE_HOUR_IN_SECONDS
- ]
- finally:
- if not _is_already_locked:
- self.state_lock.release()
-
- async def get_dollars_currently_authorized(self) -> float:
- async with self.state_lock:
- return sum(x.dollars for x in self.open_authorizations.values())
-
- async def get_dollars_authorized_and_spent(self) -> float:
- async with self.state_lock:
- dollars_currently_authorized = float(sum(x.dollars for x in self.open_authorizations.values()))
- return dollars_currently_authorized + self.dollars_spent
-
- async def get_dollars_authorized_and_spent_in_the_last_hour(self) -> float:
- async with self.state_lock:
- dollars_currently_authorized = float(sum(x.dollars for x in self.open_authorizations.values()))
- return dollars_currently_authorized + sum(x.dollars for x in self.recent_spend_events)
-
- async def bump_limits(self, limits: ResourceLimitState) -> ResourceLimitState:
- """
- Can only raise limits. This makes it easier for hammers to reason about how much they will be able to spend.
-
- If we were to allow reducing limits, we'd need to be quite careful to update existing timers, and to check
- conditions at the end of the wait loop in authorize_spend as well.
- """
- if self.parent_limits is None:
- assert limits.hard_cap_dollars < float("inf"), "Cannot unlimit spend for the top-level hammer"
-
- async with self.state_lock:
- self.hard_cap_dollars = max(self.hard_cap_dollars, limits.hard_cap_dollars)
- self.hard_cap_seconds = max(self.hard_cap_seconds, limits.hard_cap_seconds)
- self.warn_cap_dollars = max(self.warn_cap_dollars, limits.warn_cap_dollars)
- self.warn_cap_seconds = max(self.warn_cap_seconds, limits.warn_cap_seconds)
- if self.dollars_per_hour is None:
- self.dollars_per_hour = limits.dollars_per_hour
- else:
- if limits.dollars_per_hour is not None and limits.dollars_per_hour > self.dollars_per_hour:
- self.dollars_per_hour = limits.dollars_per_hour
-
- # notify anything waiting in case we just bumped what they were waiting on
- if self.dollars_per_hour and limits.dollars_per_hour is not None:
- self.limits_updated_event.set()
- self.limits_updated_event.clear()
-
- return ResourceLimitState(
- hard_cap_dollars=self.hard_cap_dollars,
- hard_cap_seconds=self.hard_cap_seconds,
- warn_cap_dollars=self.warn_cap_dollars,
- warn_cap_seconds=self.warn_cap_seconds,
- dollars_per_hour=self.dollars_per_hour,
- )
-
- def resume(self, dollars: float, limits: ResourceLimitState) -> None:
- self.hard_cap_dollars = limits.hard_cap_dollars
- self.hard_cap_seconds = limits.hard_cap_seconds
- self.warn_cap_dollars = limits.warn_cap_dollars
- self.warn_cap_seconds = limits.warn_cap_seconds
- self.dollars_per_hour = limits.dollars_per_hour
- self.dollars_spent = dollars
-
- if self.dollars_spent > self.hard_cap_dollars:
- raise DollarLimitExceeded(
- f"Have already spent {self.dollars_spent} dollars (more than the hard cap of {self.hard_cap_dollars})"
- )
-
- async def _wait_until_updated(self) -> None:
- await self.limits_updated_event.wait()
-
-
-@attr.s(auto_attribs=True)
-class HammerTimer:
- limits: ResourceLimits
- timer_started_at: datetime.datetime
- timer_task: Task | None = None
- is_timeout_warning_issued: bool = False
-
- # TODO: need to save whether or not we warned about the time so that we dont warn again when resuming
- async def on_hammer_started(self, task_group: TaskGroup, callback: Callable[[], Coroutine[Any, Any, None]]) -> None:
- seconds_ago = (get_current_time() - self.timer_started_at).total_seconds()
- possible_wait_times = [x - seconds_ago for x in [self.limits.hard_cap_seconds, self.limits.warn_cap_seconds]]
- positive_wait_times = [x for x in possible_wait_times if x > 0]
- if len(positive_wait_times) == 0:
- await callback()
- return
- time_until_limit_check = min(positive_wait_times)
- self.timer_task = task_group.create_task(self._timeout_after(time_until_limit_check, callback))
-
- async def on_hammer_stopped(self) -> None:
- # cancel the timer (if still running
- timer_task = self.timer_task
- if timer_task:
- safe_cancel(timer_task)
- try:
- await timer_task
- except CancelledError:
- pass
- self.timer_task = None
-
- async def _timeout_after(self, seconds: float, callback: Callable[[], Coroutine[Any, Any, None]]) -> None:
- while True:
- await asyncio.sleep(seconds)
-
- # re-schedule ourselves for the next check if the limits have been updated, or we were just here for a warning
- time_since_started = (get_current_time() - self.timer_started_at).total_seconds()
- if time_since_started < self.limits.hard_cap_seconds:
- # emit a warning if necessary
- if time_since_started > self.limits.warn_cap_seconds and not self.is_timeout_warning_issued:
- self.is_timeout_warning_issued = True
- await self.limits._warn(
- f"Taking longer than expected ({seconds} sec so far, will be killed at {self.limits.hard_cap_seconds}"
- )
- # figure out how long to sleep for
- seconds = min(
- [
- self.limits.hard_cap_seconds - time_since_started,
- self.limits.warn_cap_seconds - time_since_started,
- ]
- )
- continue
-
- # if we're still here, we've timed out
- await callback()
- return
-
-
-# Even if you don't use hammers, you can still benefit from having global resource limits.
-# (When in hammer-less context, this global variable is checked by LanguageModelAPI. Only the financial limits are enforced.)
-
-_GLOBAL_RESOURCE_LIMITS: ResourceLimits | None = None
-
-
-def ensure_global_resource_limits(
- *,
- max_dollars: float | None = None,
- max_seconds: float | None = None,
- warn_fraction: float | None = None,
- dollars_per_hour: float | None = None,
- reset_if_already_set: bool = False,
-) -> None:
- global _GLOBAL_RESOURCE_LIMITS
- if _GLOBAL_RESOURCE_LIMITS is None or reset_if_already_set:
- _GLOBAL_RESOURCE_LIMITS = ResourceLimits.build(
- max_dollars=max_dollars,
- max_seconds=max_seconds,
- warn_fraction=warn_fraction,
- dollars_per_hour=dollars_per_hour,
- )
- _GLOBAL_RESOURCE_LIMITS.save_spend_callback = _dummy_save_spend_callback
-
-
-def get_global_resource_limits() -> ResourceLimits | None:
- return _GLOBAL_RESOURCE_LIMITS
-
-
-async def _dummy_save_spend_callback(dollars: float) -> None:
- pass
-
-
-async def get_global_resource_limits_summary() -> str:
- if _GLOBAL_RESOURCE_LIMITS is None:
- return "No global resource limits"
- output = [
- "Global resource limits summary:",
- ]
- max_dollars = _GLOBAL_RESOURCE_LIMITS.hard_cap_dollars
- amount_spent = await _GLOBAL_RESOURCE_LIMITS.get_dollars_authorized_and_spent()
- amount_remaining = max_dollars - amount_spent
- output.append(f"- Max dollars: ${max_dollars:.4f}")
- output.append(f"- Amount spent: ${amount_spent:.4f}")
- output.append(f"- Amount remaining: ${amount_remaining:.4f}")
- return "\n".join(output)
diff --git a/imbue_core/imbue_core/async_monkey_patches.py b/imbue_core/imbue_core/async_monkey_patches.py
@@ -1,343 +0,0 @@
-import asyncio
-import sys
-import traceback
-from asyncio import Future
-from types import TracebackType
-from typing import Any
-from typing import Sequence
-
-from loguru import logger
-
-from imbue_core.errors import ExpectedError
-
-_IS_SHUTTING_DOWN = False
-
-
-def notify_task_groups_of_shutdown() -> None:
- global _IS_SHUTTING_DOWN
- _IS_SHUTTING_DOWN = True
-
-
-class PropagatingTaskGroup(asyncio.TaskGroup):
- """Improves over TaskGroup by ensuring that cancelation messages are actually propagated"""
-
- def __init__(self) -> None:
- # deferring the import in case this doesn't get used
- pass
-
- python_version: sys._version_info = sys.version_info
- if python_version[:2] != (3, 11) and python_version[:2] != (3, 12):
- raise RuntimeError(
- f"Python version 3.11 or 3.12 is required. You are using {python_version.major}.{python_version.minor}"
- )
- super().__init__()
- self._entered = False
- self._exiting = False
- self._aborting = False
- self._loop = None
- self._parent_task: asyncio.Task | None = None
- self._parent_cancel_requested = False
- self._tasks: set[asyncio.Task] = set()
- self._errors: list[BaseException] = []
- self._base_error: BaseException | None = None
- self._on_completed_fut: Future[Any] | None = None
- self._original_message: str | None = None
-
- async def __aexit__(
- self,
- et: type[BaseException] | None,
- exc: BaseException | None,
- tb: TracebackType | None,
- ) -> None:
- try:
- return await self._aexit(et, exc)
- finally:
- # Exceptions are heavy objects that can have object
- # cycles (bad for GC); let's not keep a reference to
- # a bunch of them. It would be nicer to use a try/finally
- # in __aexit__ directly but that introduced some diff noise
- self._parent_task = None
- self._errors = [] # Clear the list instead of setting to None
- self._base_error = None
- exc = None
-
- async def _aexit(self, et: type[BaseException] | None, exc: BaseException | None) -> None:
- self._exiting = True
-
- if exc is not None and self._is_base_error(exc) and self._base_error is None: # type: ignore
- self._base_error = exc
-
- propagate_cancellation_error = exc if et is asyncio.exceptions.CancelledError else None
- if self._parent_cancel_requested:
- # If this flag is set we *must* call uncancel().
- assert self._parent_task
- if self._parent_task.uncancel() == 0:
- # If there are no pending cancellations left,
- # don't propagate CancelledError.
- propagate_cancellation_error = None
-
- if et is not None:
- if not self._aborting:
- # Our parent task is being cancelled:
- #
- # async with TaskGroup() as g:
- # g.create_task(...)
- # await ... # <- CancelledError
- #
- # or there's an exception in "async with":
- #
- # async with TaskGroup() as g:
- # g.create_task(...)
- # 1 / 0
- assert exc is not None
- self._abort_and_propagate(exc)
-
- # We use while-loop here because "self._on_completed_fut"
- # can be cancelled multiple times if our parent task
- # is being cancelled repeatedly (or even once, when
- # our own cancellation is already in progress)
- while self._tasks:
- if self._on_completed_fut is None:
- assert self._loop
- self._on_completed_fut = self._loop.create_future()
-
- try:
- await self._on_completed_fut
- except asyncio.exceptions.CancelledError as ex:
- if not self._aborting:
- # Our parent task is being cancelled:
- #
- # async def wrapper():
- # async with TaskGroup() as g:
- # g.create_task(foo)
- #
- # "wrapper" is being cancelled while "foo" is
- # still running.
- propagate_cancellation_error = ex
- self._abort_and_propagate(ex)
-
- self._on_completed_fut = None
-
- assert not self._tasks
-
- if self._base_error is not None:
- try:
- raise self._base_error
- finally:
- exc = None
-
- # Propagate CancelledError if there is one, except if there
- # are other errors -- those have priority.
- try:
- if propagate_cancellation_error and not self._errors:
- try:
- raise propagate_cancellation_error
- finally:
- exc = None
- finally:
- propagate_cancellation_error = None
-
- if et is not None and et is not asyncio.exceptions.CancelledError:
- assert exc is not None
- self._errors.append(exc)
-
- if self._errors:
- try:
- raise BaseExceptionGroup(
- "unhandled errors in a TaskGroup: see earlier in logs for causal error!",
- self._errors,
- ) from None
- finally:
- exc = None
-
- def _abort(self) -> None:
- raise Exception("Please call _abort_and_propagate instead")
-
- def _abort_and_propagate(self, exc: BaseException) -> None:
- global _IS_SHUTTING_DOWN
- self._aborting = True
-
- if self._original_message is None:
- if isinstance(exc, asyncio.exceptions.CancelledError) and len(exc.args) > 0:
- self._original_message = "TaskGroup canceled because:\n" + exc.args[0]
- else:
- if not isinstance(exc, asyncio.exceptions.CancelledError):
- if not _IS_SHUTTING_DOWN and not isinstance(exc, ExpectedError):
- log_exception(
- exc,
- "Emergency print of error that caused task group to die:",
- )
- self._original_message = f"TaskGroup died because: {type(exc).__name__}: {exc}\n" + "".join(
- traceback.extract_tb(exc.__traceback__).format()
- )
-
- for t in self._tasks:
- if not t.done():
- t.cancel(self._original_message)
-
- def _on_task_done(self, task: asyncio.Task) -> None:
- self._tasks.discard(task)
-
- on_completed_fut = self._on_completed_fut
- if on_completed_fut is not None and not self._tasks:
- if not on_completed_fut.done():
- on_completed_fut.set_result(True)
-
- if task.cancelled():
- return
-
- exc = task.exception()
- if exc is None:
- return
-
- self._errors.append(exc)
- if self._is_base_error(exc) and self._base_error is None: # type: ignore
- self._base_error = exc
-
- parent_task = self._parent_task
- assert parent_task
- if parent_task.done():
- # Not sure if this case is possible, but we want to handle
- # it anyways.
- assert self._loop
- self._loop.call_exception_handler(
- {
- "message": f"Task {task!r} has errored out but its parent task {parent_task} is already completed",
- "exception": exc,
- "task": task,
- }
- )
- return
-
- if not self._aborting and not self._parent_cancel_requested:
- # If parent task *is not* being cancelled, it means that we want
- # to manually cancel it to abort whatever is being run right now
- # in the TaskGroup. But we want to mark parent task as
- # "not cancelled" later in __aexit__. Example situation that
- # we need to handle:
- #
- # async def foo():
- # try:
- # async with TaskGroup() as g:
- # g.create_task(crash_soon())
- # await something # <- this needs to be canceled
- # # by the TaskGroup, e.g.
- # # foo() needs to be cancelled
- # except Exception:
- # # Ignore any exceptions raised in the TaskGroup
- # pass
- # await something_else # this line has to be called
- # # after TaskGroup is finished.
- self._abort_and_propagate(exc)
- self._parent_cancel_requested = True
- parent_task.cancel(self._original_message)
-
-
-def safe_cancel(task: asyncio.Task, msg: str | None = None) -> None:
- """
- NOTE: this is probably not what you want! See safe_cancel_and_wait_for_cleanup below for the more common use case.
-
- Cancels a task in a way that preserves information about who canceled it.
-
- Without using this, it is super obnoxious to figure out why your function is being canceled --
- you just get a CancelledError with no traceback.
-
- We try to ensure that *all* of our tasks are canceled in this way, which makes debugging much easier.
-
- Even safe_cancel_and_wait_for_cleanup will cancel in this way. The only difference is that that function
- also waits for the task to actually be canceled. Otherwise, cancellation just enqueues a cancellation.
-
- Note also that cancellation is never guaranteed -- all it does is raise a CancelledError in the task.
- This is why it is so important to never swallow those errors!
- """
- task.is_being_canceled_by_us = True # type: ignore
- message = f"Task canceled by: \n {''.join(traceback.format_stack()[:-1])}"
- if msg:
- message += f"\nOriginal message: {msg}"
-
- task.cancel(message)
-
-
-async def safe_cancel_and_wait_for_cleanup(
- task: asyncio.Task,
- msg: str | None = None,
- exception_types_to_ignore: Sequence[type[BaseException]] = (),
-) -> None:
- """
- Convenience function for calling safe_cancel_multiple_and_wait_for_cleanup with a single task.
-
- See safe_cancel_multiple_and_wait_for_cleanup for docs.
- """
- await safe_cancel_multiple_and_wait_for_cleanup([task], msg, exception_types_to_ignore)
-
-
-async def safe_cancel_multiple_and_wait_for_cleanup(
- tasks: Sequence[asyncio.Task],
- msg: str | None = None,
- exception_types_to_ignore: Sequence[type[BaseException]] = (),
-) -> None:
- """
- Calls safe_cancel (see docs above) on each task in tasks, then waits for them to be done.
-
- Note that you can pass in a list of exception types to ignore.
- This is important for suppressing exceptions from third party libraries.
- You should probably make a constant in your project that lists these exceptions.
-
- We cannot simply suppress all BaseExceptions here because you really don't want to do that for things like signals and OutOfMemoryError
- """
- for task in tasks:
- safe_cancel(task, msg)
- # if you really want something to be canceled, you need to wait for it to be done
- # https://docs.python.org/3/library/asyncio-task.html#asyncio.Task.cancel
- results = await asyncio.gather(*tasks, return_exceptions=True)
- exceptions = []
- for result in results:
- if isinstance(result, BaseException):
- if isinstance(result, asyncio.CancelledError):
- pass
- elif isinstance(result, ExceptionGroup):
- exceptions.extend(_filter_exception_group(result))
- else:
- exceptions.append(result) # type: ignore
- # cannot do this because the task may have just finished
- # assert (
- # False
- # ), f"While cancelling async task and waiting for cleanup, expected None or CancelledError, got `{type(x)}: {x}`"
-
- filtered_exceptions = []
- for exception in exceptions:
- if not any(isinstance(exception, exception_type) for exception_type in exception_types_to_ignore):
- filtered_exceptions.append(exception)
-
- if len(filtered_exceptions) == 1:
- raise filtered_exceptions[0]
- elif len(filtered_exceptions) > 1:
- raise ExceptionGroup("Multiple exceptions in task group while canceling", filtered_exceptions)
-
-
-def _filter_exception_group(exc_group: ExceptionGroup) -> list[Exception]:
- """Recursively extract exceptions from ExceptionGroups (ignoring canceled errors)."""
- result = []
- for exc in exc_group.exceptions:
- if isinstance(exc, asyncio.CancelledError):
- continue
- elif isinstance(exc, ExceptionGroup):
- result.extend(_filter_exception_group(exc))
- else:
- result.append(exc)
- return result
-
-
-def log_exception(
- exc: BaseException,
- message: str,
- *args: Any,
- **kwargs: Any,
-) -> None:
- """Log an exception with its traceback to stderr via loguru."""
- logger.opt(exception=exc).error(message, *args, **kwargs)
-
-
-def apply() -> None:
- asyncio.TaskGroup = PropagatingTaskGroup # type: ignore
- asyncio.taskgroups.TaskGroup = PropagatingTaskGroup # type: ignore
diff --git a/imbue_core/imbue_core/caching.py b/imbue_core/imbue_core/caching.py
@@ -1,183 +0,0 @@
-from __future__ import annotations
-
-import asyncio
-from functools import lru_cache
-from pathlib import Path
-from types import TracebackType
-from typing import Generic
-from typing import Self
-from typing import Sequence
-from typing import TypeVar
-
-from diskcache import Cache
-from diskcache import JSONDisk
-
-from imbue_core.cattrs_serialization import deserialize_from_json
-from imbue_core.cattrs_serialization import serialize_to_json
-from imbue_core.frozen_utils import FrozenDict
-from imbue_core.frozen_utils import FrozenMapping
-
-ValueType = TypeVar("ValueType", covariant=True)
-
-
-class AsyncCacheInterface(Generic[ValueType]):
- async def __aenter__(self) -> Self:
- raise NotImplementedError()
-
- async def __aexit__(
- self,
- exc_type: type[BaseException] | None,
- exc_val: BaseException | None,
- exc_tb: TracebackType | None,
- ) -> None:
- raise NotImplementedError()
-
- async def set(
- self,
- key: str,
- # pyre-fixme[46]: ValueType is covariant
- value: ValueType,
- expire: int | None = None,
- read: bool = False,
- tag: str | None = None,
- retry: bool = False,
- ) -> bool:
- raise NotImplementedError()
-
- async def get(
- self,
- key: str,
- default: ValueType | None = None,
- read: bool = False,
- expire_time: bool = False,
- tag: bool = False,
- retry: bool = False,
- ) -> ValueType | None:
- raise NotImplementedError()
-
- async def get_all(
- self,
- keys: Sequence[str],
- default: ValueType | None = None,
- read: bool = False,
- expire_time: bool = False,
- tag: bool = False,
- retry: bool = False,
- ) -> FrozenMapping[str, ValueType | None]:
- raise NotImplementedError()
-
- async def get_all_keys(self, reverse: bool = False) -> tuple[str, ...]:
- raise NotImplementedError()
-
-
-class AsyncCache(AsyncCacheInterface[ValueType], Generic[ValueType]):
- def __init__(self, path: Path, value_cls: type[ValueType]) -> None:
- self.path = path
- self.value_cls = value_cls
- self.cache: Cache | None = None
-
- async def _build_cache(self) -> Cache:
- loop = asyncio.get_running_loop()
- # pyre-ignore[6]: pyre doesn't like the lru cache here
- return await loop.run_in_executor(None, get_cache, self.path)
-
- async def __aenter__(self) -> Self:
- loop = asyncio.get_running_loop()
- cache = await self._build_cache()
- self.cache = cache
- await loop.run_in_executor(None, cache.__enter__)
- return self
-
- async def __aexit__(
- self,
- exc_type: type[BaseException] | None,
- exc_val: BaseException | None,
- exc_tb: TracebackType | None,
- ) -> None:
- loop = asyncio.get_running_loop()
- cache = self.cache
- assert cache is not None
- result = await loop.run_in_executor(
- None, cache.__exit__, exc_type, exc_val, exc_tb
- )
- self.cache = None
- return result
-
- async def set(
- self,
- key: str,
- # pyre-fixme[46]: ValueType is covariant
- value: ValueType,
- expire: int | None = None,
- read: bool = False,
- tag: str | None = None,
- retry: bool = False,
- ) -> bool:
- cache = self.cache
- assert cache is not None
- loop = asyncio.get_running_loop()
- assert isinstance(value, self.value_cls), (
- f"Expected {self.value_cls}, got {type(value)}"
- )
- serialized_value = serialize_to_json(value)
- return await loop.run_in_executor(
- None, cache.set, key, serialized_value, expire, read, tag, retry
- )
-
- async def get(
- self,
- key: str,
- default: ValueType | None = None,
- read: bool = False,
- expire_time: bool = False,
- tag: bool = False,
- retry: bool = False,
- ) -> ValueType | None:
- cache = self.cache
- assert cache is not None
- loop = asyncio.get_running_loop()
- value = await loop.run_in_executor(
- None, cache.get, key, None, read, expire_time, tag, retry
- )
- if value is None:
- return default
- deserialized_value = deserialize_from_json(value)
- assert isinstance(deserialized_value, self.value_cls), (
- f"Expected {self.value_cls}, got {type(deserialized_value)}"
- )
- return deserialized_value
-
- # TODO: this is not smart implementation, but at least it will be possible to optimize later without refactoring
- async def get_all(
- self,
- keys: Sequence[str],
- default: ValueType | None = None,
- read: bool = False,
- expire_time: bool = False,
- tag: bool = False,
- retry: bool = False,
- ) -> FrozenMapping[str, ValueType | None]:
- tasks = {}
- for key in keys:
- tasks[key] = self.get(key, default, read, expire_time, tag, retry)
- results = await asyncio.gather(*tasks.values())
- return FrozenDict(zip(tasks.keys(), results))
-
- # TODO: might be nice to get iterkeys back someday, but whatever for now, too annoying to get the sync/async right
- async def get_all_keys(self, reverse: bool = False) -> tuple[str, ...]:
- cache = self.cache
- assert cache is not None
- loop = asyncio.get_running_loop()
- return tuple(await loop.run_in_executor(None, cache.iterkeys, reverse))
-
-
-@lru_cache
-def get_cache(data_path: Path) -> Cache:
- # not sure if the size limit applies when eviction is none, but ~64GB should be enough for now
- return Cache(
- str(data_path),
- disk=JSONDisk,
- disk_compress_level=0,
- eviction_policy="none",
- size_limit=2**36,
- )
diff --git a/imbue_core/imbue_core/cattrs_serialization.py b/imbue_core/imbue_core/cattrs_serialization.py
@@ -1,991 +0,0 @@
-import abc
-import asyncio
-import base64
-import builtins
-import datetime
-import functools
-import importlib
-import inspect
-import json
-from decimal import Decimal
-from enum import Enum
-from functools import cached_property
-from functools import lru_cache
-from functools import partial
-from pathlib import Path
-from pathlib import PosixPath
-from types import NoneType
-from types import UnionType
-from typing import Any
-from typing import Callable
-from typing import ForwardRef
-from typing import Hashable
-from typing import Mapping
-from typing import TypeVar
-from typing import Union
-from typing import cast
-from typing import get_origin
-from uuid import UUID
-
-import anyio
-import attr
-from cachetools import LRUCache
-from cattrs import Converter
-from cattrs._compat import is_generic
-from cattrs.gen import make_dict_unstructure_fn
-from cattrs.gen import override
-from httpx import URL
-from humps import camelize # pyre-ignore[21]: pyre doesn't understand this import
-from pydantic import BaseModel
-from pydantic_core import PydanticUndefined
-
-from imbue_core.errors import ImbueError
-from imbue_core.fixed_traceback import FixedTraceback
-from imbue_core.frozen_utils import FrozenDict
-from imbue_core.frozen_utils import FrozenMapping
-from imbue_core.serialization import SerializedException
-from imbue_core.serialization_types import Serializable
-
-T = TypeVar("T")
-TYPE_KEY = "__type"
-EXCEPTION_KEY = "__exception"
-
-# LABELS for marking attributes with special handling
-DONT_SERIALIZE_METADATA_KEY = "_imbue_dont_serialize"
-DONT_SERIALIZE = {DONT_SERIALIZE_METADATA_KEY: True}
-SERIALIZE_WITH_DEFAULT_KEY = "_imbue_serialize_with_default"
-SERIALIZE_WITH_DEFAULT = {SERIALIZE_WITH_DEFAULT_KEY: True}
-
-SERIALIZABLE_PROPERTY_KEY = "_imbue_is_serializable_property"
-CACHED_SERIALIZABLE_PROPERTY_KEY = "_imbue_is_cached_serializable_property"
-
-
-##########################################################################################
-# UTILITY FUNCTIONS
-##########################################################################################
-
-
-def _safe_issubclass(t1: type, t2: type) -> bool:
- return inspect.isclass(t1) and issubclass(t1, t2)
-
-
-def _is_frozen_mapping_type(t: type) -> bool:
- return _safe_issubclass(get_origin(t) or t, FrozenMapping)
-
-
-def _is_mapping_type(t: type) -> bool:
- return _safe_issubclass(get_origin(t) or t, Mapping)
-
-
-_ALLOWED_SPECIAL_MAPPING_TYPES = (LRUCache,)
-
-
-def _is_special_mapping_type(t: type) -> bool:
- return t in _ALLOWED_SPECIAL_MAPPING_TYPES
-
-
-def _is_str_type_special_mapping_type(t: str) -> bool:
- return t in [_type_to_string(t, fully_qualified=True) for t in _ALLOWED_SPECIAL_MAPPING_TYPES]
-
-
-def _is_obj_supported_primitive(obj: Any) -> bool:
- return type(obj) in {bool, int, float, str, NoneType}
-
-
-def _type_to_string(t: type, fully_qualified: bool) -> str:
- name = t.__name__
- if fully_qualified:
- return f"{t.__module__}.{name}"
- else:
- return name
-
-
-def _type_from_string(type_str: str) -> Any:
- if "[" in type_str:
- class_details, _ = type_str.split("[", 1)
- else:
- class_details = type_str
- if "." in class_details:
- module_path, class_name = class_details.rsplit(".", 1)
- module = importlib.import_module(module_path)
- else:
- class_name = class_details
- module = builtins
- result = getattr(module, class_name)
- return result
-
-
-def get_serializable_properties(obj: Any) -> dict[str, Any]:
- members = inspect.getmembers(type(obj))
- marked_members = {}
- for name, member in members:
- if is_serializable_property(member):
- marked_members[name] = getattr(obj, name)
- return marked_members
-
-
-def is_serializable_property(func: Callable) -> bool:
- return getattr(func, CACHED_SERIALIZABLE_PROPERTY_KEY, False) or (
- isinstance(func, property) and getattr(func.fget, SERIALIZABLE_PROPERTY_KEY, False)
- )
-
-
-def cached_serializable_property(func: Callable[..., T]) -> cached_property[T]:
- property_to_return = cached_property(func)
- setattr(property_to_return, CACHED_SERIALIZABLE_PROPERTY_KEY, True)
- return property_to_return
-
-
-def serializable_property(func: Callable[..., T]) -> property:
- property_to_return = func
- # NOTE: this will be stored in the fget attribute of the property, which is also the function
- # we are decorating, so we must check in `func.fget` to see if the property is serializable.
- # We need to do it this way because we cannot set the attribute on the property object/wrapper
- # itself, because of the way the inbuilt `property` decorator works.
- setattr(property_to_return, SERIALIZABLE_PROPERTY_KEY, True)
- return property(property_to_return)
-
-
-def get_dont_serialize_member_names_of_type(obj_type: type) -> list[str]:
- if not attr.has(obj_type):
- return []
- return [field.name for field in attr.fields(obj_type) if field.metadata.get(DONT_SERIALIZE_METADATA_KEY, False)]
-
-
-def get_serialize_with_default_member_names_of_type(
- obj_type: type,
-) -> Mapping[str, Any]:
- if _safe_issubclass(obj_type, BaseModel):
- model_fields = getattr(obj_type, "model_fields", {})
- return {
- name: None if field.default == PydanticUndefined else field.default for name, field in model_fields.items()
- }
- if not attr.has(obj_type):
- return {}
- return {
- field.name: None if field.default == attr.NOTHING else field.default
- for field in attr.fields(obj_type)
- if field.metadata.get(SERIALIZE_WITH_DEFAULT_KEY, False)
- }
-
-
-def get_dont_serialize_member_names(obj: Any) -> list[str]:
- if not attr.has(obj):
- return []
- members = inspect.getmembers(obj)
- marked_members = []
- for name, _ in members:
- if is_dont_serialize_member(obj, name):
- marked_members.append(name)
- return marked_members
-
-
-def is_dont_serialize_member(obj: Any, member_name: str) -> bool:
- if not attr.has(obj):
- return False
- for field in attr.fields(obj.__class__): # type: ignore
- if field.name == member_name:
- return bool(field.metadata.get(DONT_SERIALIZE_METADATA_KEY, False))
- return False
-
-
-class SerializationError(ImbueError):
- """Raised when we encounter problems related to Serialization or Deserialization."""
-
-
-def _to_json_dumpable_object_without_type_keys(data: Any) -> Any:
- if isinstance(data, dict):
- if data.get(TYPE_KEY, "") in {
- _type_to_string(PosixPath, fully_qualified=True),
- _type_to_string(Path, fully_qualified=True),
- _type_to_string(UUID, fully_qualified=True),
- }:
- return data["value"]
- else:
- return {
- key: _to_json_dumpable_object_without_type_keys(value) for key, value in data.items() if key != TYPE_KEY
- }
- elif isinstance(data, list):
- return [_to_json_dumpable_object_without_type_keys(item) for item in data]
- elif _is_obj_supported_primitive(data):
- return data
- else:
- return str(data)
-
-
-def _camelize_keys_which_represent_python_names(data: Any) -> Any:
- """Converts JSON-style objects to use camel case keys.
-
- Takes a JSON-style object produced by CONVERTER.structure and returns the same object with certain
- keys converted to camel case. Camel cases keys which are derived from names of Python attributes and properties.
- Does not camel-case keys which were keys of dictionaries before serialization.
-
- See cattrs_serialization_test.test_camel_casing for an example.
- """
- if isinstance(data, dict):
- if TYPE_KEY not in data or issubclass(_type_from_string(data[TYPE_KEY]), Mapping):
- return {key: _camelize_keys_which_represent_python_names(value) for key, value in data.items()}
- else:
- # pyre-ignore[16]: pyre doesn't understand the import of camelize
- return {camelize(key): _camelize_keys_which_represent_python_names(value) for key, value in data.items()}
- elif isinstance(data, list):
- return [_camelize_keys_which_represent_python_names(item) for item in data]
- else:
- return data
-
-
-##########################################################################################
-# CLASS-SPECIFIC HOOKS
-##########################################################################################
-
-
-class _ShouldDeserialize:
- pass
-
-
-# FIXME: Types such as LRUCache will always serialize without errors since they inherit from Mapping but will not deserialize correctly.
-# We should either document this behavior or change it so that the serialization fails if the type is not supported.
-def _serialize_mapping_to_json_dict(data: Mapping, converter: Converter) -> Any:
- assert _is_mapping_type(type(data)), f"Attempted to serialize object of type {type(data)} as a mapping."
- return {str(converter.unstructure(k)): converter.unstructure(v) for k, v in data.items()}
-
-
-def _serialize_mapping(data: Mapping, converter: Converter) -> Any:
- assert _is_mapping_type(type(data)), f"Attempted to serialize object of type {type(data)} as a mapping."
- entries = [(converter.unstructure(k), converter.unstructure(v)) for k, v in data.items()]
- return {
- TYPE_KEY: _type_to_string(type(data), fully_qualified=True),
- "__entries": entries,
- }
-
-
-def _deserialize_special_mapping_types(data: dict, type_key: str) -> Mapping:
- if type_key == _type_to_string(LRUCache, fully_qualified=True):
- # FIXME: We're not serializing the object correctly and so the deserialization is hacky
- obj: LRUCache = LRUCache(maxsize=10000)
- return obj
- else:
- raise ValueError(f"Unsupported type {type_key}")
-
-
-def _deserialize_mapping(data: dict, mapping_type: type, converter: Converter) -> Mapping:
- if TYPE_KEY in data and _is_str_type_special_mapping_type(data[TYPE_KEY]):
- return _deserialize_special_mapping_types(data, data[TYPE_KEY])
-
- out = {}
- if "__entries" in data:
- entries = data["__entries"]
- else:
- # We keep this branch for backwards compatibility with mappings serialized as dictionaries.
- # We do not support Yasoo's DictWithSerializedKeys -- those will need to be migrated to the new format.
- if TYPE_KEY in data:
- del data[TYPE_KEY]
- entries = data.items()
-
- for k, v in entries:
- out[converter.structure(k, _ShouldDeserialize)] = converter.structure(v, _ShouldDeserialize)
-
- if _is_frozen_mapping_type(mapping_type):
- return FrozenDict(out)
- return out
-
-
-def _serialize_frozen_set(data: frozenset, converter: Converter) -> dict:
- assert type(data) is frozenset, f"Attempted to serialize object of type {type(data)} as a frozenset."
- value = converter.unstructure(data, unstructure_as=list)
- return {"value": value, TYPE_KEY: _type_to_string(type(data), fully_qualified=True)}
-
-
-def _deserialize_frozen_set(data: dict, _: type, converter: Converter) -> frozenset:
- return frozenset(converter.structure(data["value"], list))
-
-
-def _serialize_uuid(data: UUID) -> dict:
- if type(data) is UUID:
- return {
- "value": data.hex,
- TYPE_KEY: _type_to_string(type(data), fully_qualified=True),
- }
- elif type(data) is str:
- return {"value": data, TYPE_KEY: _type_to_string(UUID, fully_qualified=True)}
- else:
- raise TypeError("Tried to serialize " + str(data) + ", which is neither a string nor a UUID, as a UUID.")
-
-
-def _deserialize_uuid(data: dict[str, str] | str, _: type) -> UUID:
- if isinstance(data, dict):
- return UUID(data["value"])
- elif isinstance(data, str):
- return UUID(data)
- else:
- raise TypeError("Tried to deserialize something which is neither a string nor a dictionary, as a UUID.")
-
-
-def _serialize_tuple(data: tuple, converter: Converter) -> dict:
- assert type(data) is tuple, f"Attempted to serialize object of type {type(data)} as a tuple."
- return {
- "value": [converter.unstructure(x) for x in data],
- TYPE_KEY: _type_to_string(type(data), fully_qualified=True),
- }
-
-
-def _deserialize_tuple(data: dict, _: type, converter: Converter) -> tuple:
- return tuple(converter.structure(x, _ShouldDeserialize) for x in data["value"])
-
-
-def _serialize_url(data: URL) -> dict:
- assert type(data) is URL, f"Tried to serialize {data} which is not a URL."
- return {
- "value": str(data),
- TYPE_KEY: _type_to_string(type(data), fully_qualified=True),
- }
-
-
-def _deserialize_url(data: dict, _: type) -> URL:
- return URL(data["value"])
-
-
-def _serialize_decimal(data: Decimal) -> dict:
- assert type(data) is Decimal, f"Attempted to serialize object of type {type(data)} as a Decimal."
- return {
- "value": str(data),
- TYPE_KEY: _type_to_string(type(data), fully_qualified=True),
- }
-
-
-def _deserialize_decimal(data: dict, _: type) -> Decimal:
- return Decimal(data["value"])
-
-
-def _serialize_traceback(data: FixedTraceback) -> dict:
- assert _safe_issubclass(
- type(data), FixedTraceback
- ), f"Attempted to serialize object of type {type(data)} as a traceback."
- return {
- "value": data.to_dict(),
- TYPE_KEY: _type_to_string(type(data), fully_qualified=True),
- }
-
-
-def _deserialize_traceback(data: dict, _: type) -> FixedTraceback:
- return FixedTraceback.from_dict(data["value"])
-
-
-def _serialize_path(data: Path) -> dict:
- assert _safe_issubclass(type(data), Path), f"Attempted to serialize an object of type {type(data)} as a Path."
- return {
- "value": str(data),
- TYPE_KEY: _type_to_string(type(data), fully_qualified=True),
- }
-
-
-def _deserialize_path(data: Any, _: type) -> Path:
- if type(data) is dict:
- return Path(data["value"])
- return Path(data)
-
-
-def _serialize_anyio_path(data: anyio.Path) -> dict:
- assert _safe_issubclass(type(data), anyio.Path), f"Attempted to serialize an object of type {type(data)} as a Path."
- return {
- "value": str(data),
- TYPE_KEY: _type_to_string(type(data), fully_qualified=True),
- }
-
-
-def _deserialize_anyio_path(data: Any, _: type) -> anyio.Path:
- if type(data) is dict:
- return anyio.Path(data["value"])
- return anyio.Path(data)
-
-
-def _serialize_datetime(data: datetime.datetime) -> dict:
- assert _safe_issubclass(
- type(data), datetime.datetime
- ), f"Attempted to serialize object of type {type(data)} as a datetime."
- return {
- TYPE_KEY: _type_to_string(type(data), fully_qualified=True),
- "time": data.astimezone(datetime.timezone.utc).timestamp(),
- "tzaware": data.tzinfo is not None,
- }
-
-
-def _deserialize_datetime(data: dict, _: type) -> datetime.datetime:
- return datetime.datetime.fromtimestamp(data["time"], datetime.timezone.utc if data.get("tzaware", None) else None)
-
-
-def _serialize_bytes(data: bytes) -> dict:
- assert type(data) is bytes, f"Attempted to serialize object of type {type(data)} as bytes."
- return {
- TYPE_KEY: _type_to_string(type(data), fully_qualified=True),
- # use ascii since base64 guarantees ascii characters only
- "value": base64.b64encode(data).decode("ascii"),
- }
-
-
-def _deserialize_bytes(data: dict, _: type) -> bytes:
- return base64.b64decode(data["value"])
-
-
-def _is_forward_ref(t: type) -> bool:
- return isinstance(t, ForwardRef)
-
-
-def _serialize_forward_ref(data: Any, converter: Converter) -> Any:
- return converter.unstructure(data, unstructure_as=type(data))
-
-
-def _deserialize_forward_ref(data: Any, _: type, converter: Converter) -> Any:
- # TODO: think of a way to evaluate the ForwardRef _, to improve type safety.
- # Once we do that, we can swap out the evaluated type for ShouldDeserialize
- # and enforce that we're getting an object of the correct type.
- return _deserialize_serialized_object(data, _ShouldDeserialize, converter)
-
-
-def _is_union_type(t: type) -> bool:
- origin = get_origin(t)
- return origin is Union or origin is UnionType
-
-
-def _deserialize_union_type(data: Any, type_of_data: type, converter: Converter) -> Any:
- return converter.structure(data, _ShouldDeserialize)
-
-
-def _serialize_enum(data: Enum, converter: Converter) -> Any:
- assert inspect.isclass(type(data)) and issubclass(
- type(data), Enum
- ), f"Attempted to serialize object of type {type(data)} as an Enum."
- return converter._unstructure_enum(data)
-
-
-def _deserialize_enum(data: dict[str, str] | str, t: type) -> Any:
- # We include this complicated logic to preserve backwards compatibility with old JSON that was
- # serialized by Yasoo. Yasoo serialized enums by converting them into the form
- # {"__type": "...", "value": "..."}. Strangely, Yasoo converted this dictionary into a string
- # whenever an enum value occurred as a dictionary key, but did not convert it into a string
- # when it occurred anywhere else. Hence we need to handle enums that are represented by
- # dictionaries, stringified dictionaries, and strings.
-
- assert _safe_issubclass(t, Enum)
-
- if isinstance(data, str):
- try:
- # This is the case where data is an enum value, serialized by Cattrs.
- return t(data)
- except ValueError:
- # This is the case where data is a stringified dictionary, serialized by Yasoo.
- data_as_dict = json.loads(data)
- return t[data_as_dict["value"]] # type: ignore
- else:
- # This is the case where data is a dictionary, serialized by Yasoo.
- return t[data["value"]] # type: ignore
-
-
-##########################################################################################
-# TYPE KEY LOGIC
-##########################################################################################
-
-
-class _AvoidTypeKeyLogic:
- pass
-
-
-@lru_cache
-def flag_to_ignore_type_key_hooks(t: type) -> type:
- class GivenTypeFlaggedToAvoidTypeKeyLogic(t, _AvoidTypeKeyLogic):
- pass
-
- GivenTypeFlaggedToAvoidTypeKeyLogic.__name__ = t.__name__
- GivenTypeFlaggedToAvoidTypeKeyLogic.__qualname__ = t.__qualname__
-
- # pyre-fixme[16]: pyre doesn't understand dynamically created classes
- return GivenTypeFlaggedToAvoidTypeKeyLogic
-
-
-def get_pydantic_model_attributes(model: BaseModel) -> dict[str, Any]:
- # This is a hack to dump only the top level but also avoid dumping any properties
- attributes = getattr(type(model), "model_fields", {})
- return {a: getattr(model, a) for a in attributes}
-
-
-# These two factory functions produce the functions for serializing attr classes.
-# Only one of them should be registered at a time, depending on whether we are including
-# do-not-serialize fields in the serialization.
-def _serialize_attr_class_factory(cls: type, converter: Converter) -> Callable[[Any], Any]:
- return make_dict_unstructure_fn(cls, converter)
-
-
-def _serialize_attr_class_without_dont_serialize_fields(
- cls: type, converter: Converter, is_camel_case: bool
-) -> Callable[[Any], Any]:
- members_to_omit = get_dont_serialize_member_names_of_type(cls)
- omit_kwargs = {name: override(omit=True) for name in members_to_omit}
- return make_dict_unstructure_fn(cls, converter, **omit_kwargs) # type: ignore
-
-
-def _serialize_with_type_key(data: Any, converter: Converter, for_javascript: bool = False) -> Any:
- type_of_data = type(data)
-
- if _is_obj_supported_primitive(data) or isinstance(data, list) or isinstance(data, tuple):
- # This means that data was annotated as a Serializable, but it is a primitive or a tuple.
- return converter.unstructure(data, unstructure_as=type_of_data)
-
- type_of_data_with_typekey_already_added = flag_to_ignore_type_key_hooks(type_of_data) # type: ignore
-
- # This is a hack which is necessary because cattrs does not work well with Protocols.
- # Protocols are generic classes, but they don't have __orig_bases__, which cattrs
- # assumes them to have.
- if is_generic(type_of_data_with_typekey_already_added):
- old_orig_bases = getattr(type_of_data_with_typekey_already_added, "__orig_bases__", ())
- setattr(type_of_data_with_typekey_already_added, "__orig_bases__", old_orig_bases)
-
- if isinstance(data, BaseModel):
- # This is a shortcut: when you encounter a Pydantic model, just use Pydantic serialization.
- # NOTE: currently we don't support `DONT_SERIALIZE` fields in pydantic models.
- # so we just serialize all fields.
- unstructured = data.model_dump(by_alias=for_javascript, mode="json")
- else:
- unstructured = converter.unstructure(data, unstructure_as=type_of_data_with_typekey_already_added)
-
- assert isinstance(unstructured, dict)
-
- if for_javascript:
- unstructured.update({k: converter.unstructure(v) for k, v in get_serializable_properties(data).items()})
-
- return {
- TYPE_KEY: _type_to_string(type_of_data, fully_qualified=True),
- **unstructured,
- }
-
-
-# This is the predicate used in the factory functions above, so they trigger for serializable and attr classes
-# that have had their type key logic handled.
-def _should_serialize_without_type_key(t: type) -> bool:
- is_serializable_class = _safe_issubclass(t, Serializable) or attr.has(t) or _safe_issubclass(t, BaseModel)
- return is_serializable_class and _safe_issubclass(t, _AvoidTypeKeyLogic)
-
-
-def _should_add_type_key(t: type) -> bool:
- is_serializable_class = _safe_issubclass(t, Serializable) or attr.has(t) or _safe_issubclass(t, BaseModel)
- return is_serializable_class and not _safe_issubclass(t, _AvoidTypeKeyLogic)
-
-
-def _deserialize_serialized_object(data: Any, type_of_data: type, converter: Converter) -> Any:
- if isinstance(data, list):
- # Data is a list of objects.
- return converter.structure(data, list[_ShouldDeserialize])
- elif not isinstance(data, Mapping):
- # Data is a primitive, like an integer or a string.
- return converter.structure(data, type(data))
- else:
- # Data is a dictionary with a type key, representing an attrs object, a Pydantic model, or a Mapping
- return _deserialize_using_type_marker(data, type_of_data, converter)
-
-
-def _should_deserialize_with_type_key_logic(t: type) -> bool:
- is_type_that_should_be_deserialized = (
- attr.has(t)
- or _safe_issubclass(t, Serializable)
- or _safe_issubclass(t, _ShouldDeserialize)
- or t is Hashable
- or _is_mapping_type(t)
- or _safe_issubclass(t, BaseModel)
- )
- should_avoid_type_key_logic = _safe_issubclass(t, _AvoidTypeKeyLogic) or _safe_issubclass(
- get_origin(t) or NoneType, _AvoidTypeKeyLogic
- )
- return is_type_that_should_be_deserialized and not should_avoid_type_key_logic
-
-
-def deserialized_object_violates_target_type(obj: Any, target_type: type) -> bool:
- if target_type is _ShouldDeserialize or target_type is Serializable:
- return False
- if type(target_type) is TypeVar:
- # We're not really able to check if the object is an instance of a type that's behind a TypeVar.
- return False
- return not isinstance(obj, get_origin(target_type) or target_type)
-
-
-# Note that expected_type_based_on_annotations may be much more vague than the actual type of the object.
-# For example: it may be Serializable, when the object is supposed to be
-# deserialized as a HammerResult. We get the real type from the "__type" key.
-def _deserialize_using_type_marker(
- obj: Mapping[Any, Any],
- expected_type_based_on_annotations: type[T],
- converter: Converter,
-) -> T:
- if TYPE_KEY in obj:
- type_of_obj = _type_from_string(obj[TYPE_KEY])
- else:
- type_of_obj = expected_type_based_on_annotations
-
- if _is_special_mapping_type(type_of_obj):
- pass
- elif _is_frozen_mapping_type(type_of_obj):
- obj.pop(TYPE_KEY, None) # type: ignore
- type_of_obj = FrozenMapping[_ShouldDeserialize, _ShouldDeserialize]
- elif _is_mapping_type(type_of_obj):
- obj.pop(TYPE_KEY, None) # type: ignore
- type_of_obj = dict[_ShouldDeserialize, _ShouldDeserialize]
- elif _safe_issubclass(type_of_obj, BaseModel):
- assert isinstance(obj, dict)
- obj.pop(TYPE_KEY, None)
- return cast(T, type_of_obj.model_validate(obj))
- elif not attr.has(type_of_obj):
- # This happens when there is a primitive object which is annotated as Serializable.
- return converter.structure(obj, type_of_obj) # type: ignore
-
- # By mixing in the "avoid type key logic" class, force cattrs to do its normal behavior.
- ret: T = converter.structure(obj, flag_to_ignore_type_key_hooks(type_of_obj))
-
- if inspect.isclass(type_of_obj):
- # Upcast the result so that it has the correct type again, without the mixin.
- object.__setattr__(ret, "__class__", type_of_obj)
-
- if deserialized_object_violates_target_type(ret, expected_type_based_on_annotations):
- raise TypeError(
- f"Tried to deserialize into type {expected_type_based_on_annotations}, but got object of type {type(ret)}"
- )
-
- return ret
-
-
-def _resolve_default(default: Any) -> Any:
- if isinstance(default, attr.Factory): # type: ignore
- return default.factory()
- return default
-
-
-def _serialize_with_defaults(cls: type, converter: Converter) -> Callable[[Any], Any]:
- # Handle a pydantic model
- if _safe_issubclass(cls, BaseModel):
- return lambda x: {k: converter.unstructure(v) for k, v in get_pydantic_model_attributes(x).items()}
-
- members_with_defaults = get_serialize_with_default_member_names_of_type(cls)
- overriden_kwargs = {
- name: override(unstruct_hook=(lambda _, value=_resolve_default(default): value)) # type: ignore
- for name, default in members_with_defaults.items()
- }
- return make_dict_unstructure_fn(cls, converter, **overriden_kwargs) # type: ignore
-
-
-def _should_serialize_as_serialized_exception(t: type) -> bool:
- return (
- _safe_issubclass(get_origin(t) or t, BaseException) and not attr.has(t) and not _safe_issubclass(t, BaseModel)
- )
-
-
-##########################################################################################
-# CONVERTER FACTORY
-##########################################################################################
-
-
-class _ConverterFactory:
- """Factory for creating converters with different configurations.
-
- e.g. for serializing to javascript, or python, or to include do-not-serialize fields.
- """
-
- def build_base_converter(self) -> Converter:
- # Builds of new base converter object, which registers all the hooks that are common to all converters.
- # The idea being that all new converters start from this base and then override hooks they need to change
- # NOTE: we need to generate a new converter object for each independent concrete converter (as opposed to
- # using converter.copy()) since we use partial functions/closures and this way we ensure the function is
- # being called with the correct converter object.
- converter = Converter()
-
- converter.register_structure_hook_func(_is_mapping_type, partial(_deserialize_mapping, converter=converter))
- # serialization of mapping types depends on the specific converter so is done in the get_converter factory method
-
- converter.register_unstructure_hook(frozenset, partial(_serialize_frozen_set, converter=converter))
- converter.register_structure_hook(frozenset, partial(_deserialize_frozen_set, converter=converter))
-
- converter.register_unstructure_hook(UUID, _serialize_uuid)
- converter.register_structure_hook(UUID, _deserialize_uuid)
-
- converter.register_unstructure_hook(URL, _serialize_url)
- converter.register_structure_hook(URL, _deserialize_url)
-
- converter.register_unstructure_hook(Decimal, _serialize_decimal)
- converter.register_structure_hook(Decimal, _deserialize_decimal)
-
- converter.register_unstructure_hook(FixedTraceback, _serialize_traceback)
- converter.register_structure_hook(FixedTraceback, _deserialize_traceback)
-
- converter.register_unstructure_hook(Path, _serialize_path)
- converter.register_structure_hook(Path, _deserialize_path)
-
- converter.register_unstructure_hook(anyio.Path, _serialize_anyio_path)
- converter.register_structure_hook(anyio.Path, _deserialize_anyio_path)
-
- converter.register_unstructure_hook(datetime.datetime, _serialize_datetime)
- converter.register_structure_hook(datetime.datetime, _deserialize_datetime)
-
- converter.register_unstructure_hook(bytes, _serialize_bytes)
- converter.register_structure_hook(bytes, _deserialize_bytes)
-
- converter.register_unstructure_hook(PosixPath, _serialize_path)
- converter.register_structure_hook(PosixPath, _deserialize_path)
-
- converter.register_unstructure_hook_func(_is_forward_ref, partial(_serialize_forward_ref, converter=converter))
- converter.register_structure_hook_func(_is_forward_ref, partial(_deserialize_forward_ref, converter=converter))
-
- converter.register_structure_hook_func(_is_union_type, partial(_deserialize_union_type, converter=converter))
-
- converter.register_structure_hook(NoneType, lambda data, _: None)
-
- converter.register_unstructure_hook(Enum, partial(_serialize_enum, converter=converter))
- converter.register_structure_hook(Enum, _deserialize_enum)
-
- converter.register_unstructure_hook_func(
- _should_serialize_as_serialized_exception,
- lambda e: serialize_to_dict(
- SerializedException.build(e),
- use_defaults_for_unserializable_fields=True,
- ),
- )
-
- converter.register_structure_hook_func(
- _should_deserialize_with_type_key_logic,
- partial(_deserialize_serialized_object, converter=converter),
- )
-
- converter.register_structure_hook_func(
- lambda t: isinstance(t, TypeVar),
- partial(_deserialize_serialized_object, converter=converter),
- )
-
- return converter
-
- def get_converter_with_defaults(self, converter: Converter) -> Converter:
- converter.register_unstructure_hook(asyncio.Lock, lambda _: None)
- converter.register_structure_hook(asyncio.Lock, lambda data, _: asyncio.Lock())
-
- converter.register_unstructure_hook(asyncio.Task, lambda _: None)
- converter.register_structure_hook(asyncio.Task, lambda data, _: None)
-
- converter.register_unstructure_hook(asyncio.Queue, lambda _: None)
- converter.register_structure_hook(asyncio.Queue, lambda data, _: None)
-
- converter.register_unstructure_hook(asyncio.Event, lambda _: None)
- converter.register_structure_hook(asyncio.Event, lambda data, _: None)
-
- converter.register_unstructure_hook(asyncio.Semaphore, lambda _: None)
- converter.register_structure_hook(asyncio.Semaphore, lambda data, _: None)
-
- converter.register_unstructure_hook(abc.ABCMeta, lambda _: None)
- converter.register_structure_hook(abc.ABCMeta, lambda data, _: None)
- converter.register_unstructure_hook_factory(
- _should_serialize_without_type_key,
- partial(_serialize_with_defaults, converter=converter),
- )
-
- return converter
-
- @functools.cache
- def get_converter(
- self,
- for_javascript: bool = False,
- exclude_dont_serialize_fields: bool = False,
- use_defaults_for_unserializable_fields: bool = False,
- ) -> Converter:
- """Returns a converter with the given configuration.
-
- The result of this method is cached, so subsequent calls with the same arguments will return the same converter.
- """
- assert not (
- exclude_dont_serialize_fields and use_defaults_for_unserializable_fields
- ), f"Expected exactly one flag to be set, got {exclude_dont_serialize_fields=}, {use_defaults_for_unserializable_fields=}"
-
- converter = self.build_base_converter()
- if for_javascript:
- converter.register_unstructure_hook_func(
- _is_mapping_type,
- partial(_serialize_mapping_to_json_dict, converter=converter),
- )
- else:
- converter.register_unstructure_hook_func(_is_mapping_type, partial(_serialize_mapping, converter=converter))
- converter.register_unstructure_hook(tuple, partial(_serialize_tuple, converter=converter))
- converter.register_structure_hook(tuple, partial(_deserialize_tuple, converter=converter))
-
- if exclude_dont_serialize_fields:
- converter.register_unstructure_hook_factory(
- _should_serialize_without_type_key,
- partial(
- _serialize_attr_class_without_dont_serialize_fields,
- converter=converter,
- is_camel_case=for_javascript,
- ),
- )
- else:
- converter.register_unstructure_hook_factory(
- _should_serialize_without_type_key,
- partial(_serialize_attr_class_factory, converter=converter),
- )
- if use_defaults_for_unserializable_fields:
- converter = self.get_converter_with_defaults(converter)
-
- converter.register_unstructure_hook_func(
- _should_add_type_key,
- partial(
- _serialize_with_type_key,
- converter=converter,
- for_javascript=for_javascript,
- ),
- )
-
- return converter
-
-
-CONVERTER_FACTORY = _ConverterFactory()
-
-
-##########################################################################################
-# ENTRY POINTS
-##########################################################################################
-
-
-def _serialize_to_json_dumpable_object(
- obj: Any,
- is_reversible: bool = True,
- for_javascript: bool = False,
- exclude_dont_serialize_fields: bool = False,
- use_defaults_for_unserializable_fields: bool = False,
-) -> Any:
- if exclude_dont_serialize_fields:
- # Check and raise error to make it clear to the caller that the object cannot be deserialized.
- # This is a sanity check, to make it easier to debug when using do-not-serialize fields.
- # NOTE: this will only catch cases where non-serializable fields are in obj, but not cases where
- # the non-serializable fields are in nested objects, checking for the nested case is a little complicated
- # so we don't do it basically.
- assert (
- not is_reversible
- ), "Cannot deserialize object when excluding do-not-serialize fields (i.e. when `exclude_dont_serialize_fields=True`). If you want to serialize an object and exclude do-not-serialize fields, make sure to set `is_reversible=False`."
-
- if use_defaults_for_unserializable_fields:
- # The point of the use_defaults_for_unserializable_fields flag is to make it possible to serialize objects
- # and then recreate them later even if certain fields are not fully saved. We never want to use this flag
- # with `is_reversible=False` since we won't know the type to be able to recreate the object.
- assert is_reversible, "Cannot restructure inputs if is_reversible=False"
-
- # TODO: this is a hack to make it possible to serialize ExecutionContexts for class methods.
- # This lets us serialize ExecutionContexts for calls to class methods without serializing the class itself.
- # The long-term solution is to write a custom hook that can serialize type objects.
- if type(obj) is dict and "__class__" in obj:
- del obj["__class__"]
-
- converter = CONVERTER_FACTORY.get_converter(
- for_javascript=for_javascript,
- exclude_dont_serialize_fields=exclude_dont_serialize_fields,
- use_defaults_for_unserializable_fields=use_defaults_for_unserializable_fields,
- )
-
- dict_result = converter.unstructure(obj)
- if for_javascript:
- dict_result = _camelize_keys_which_represent_python_names(dict_result)
-
- if not is_reversible:
- return _to_json_dumpable_object_without_type_keys(dict_result)
-
- return dict_result
-
-
-def serialize_to_dict(
- obj: Any,
- is_reversible: bool = True,
- for_javascript: bool = False,
- exclude_dont_serialize_fields: bool = False,
- use_defaults_for_unserializable_fields: bool = False,
-) -> dict[str, Any]:
- """Serialize to a python dict."""
- return cast(
- dict[str, Any],
- _serialize_to_json_dumpable_object(
- obj,
- is_reversible=is_reversible,
- for_javascript=for_javascript,
- exclude_dont_serialize_fields=exclude_dont_serialize_fields,
- use_defaults_for_unserializable_fields=use_defaults_for_unserializable_fields,
- ),
- )
-
-
-def serialize_to_json(
- obj: Any,
- indent: int | None = None,
- sort_keys: bool = False,
- is_reversible: bool = True,
- for_javascript: bool = False,
- exclude_dont_serialize_fields: bool = False,
- use_defaults_for_unserializable_fields: bool = False,
-) -> str:
- """Serialize an object to a JSON string.
-
- This is the main serialization entrypoint.
-
- `is_reversible` controls whether we enforce that the result can be deserialized. In some cases we don't care about
- reversibility, e.g. when serializing data for a frontend we often don't care whether we can deserialize.
-
- `for_javascript` controls whether we use camelCase for keys that originally were Python identifiers.
-
- `exclude_dont_serialize_fields` controls whether we include do-not-serialize fields in the serialization.
- If this is `False` then any attr class fields marked with as don't serialize, e.g. with `attr.ib(metadata=DONT_SERIALIZE)`,
- will still be included in the serialization. If this is `True` then they will be excluded, however this also means that
- the result will not be reversible (and thus the caller will have to set `is_reversible=False`).
-
- `use_defaults_for_unserializable_fields` controls whether we fill fields that cannot be serialized with their default values.
- IMPORTANT: If you use this flag, data may be discarded during deserialization.
- The goal is to be able to deserialize fields to the original type without caring about the data contained.
- Default value choices (guided by crafty serialization requirements):
- - Fields that are marked with attr.ib(metadata=SERIALIZE_WITH_DEFAULT) have the following default values:
- - Fields that are marked with `attr.ib(default=...)` or `attr.ib(factory=...)` use their default values.
- - Fields that do not have a default value are filled with None.
- - Asyncio objects are filled with None.
- - Exceptions are replaced with a string representation
- """
- try:
- unstructured = _serialize_to_json_dumpable_object(
- obj,
- is_reversible=is_reversible,
- for_javascript=for_javascript,
- exclude_dont_serialize_fields=exclude_dont_serialize_fields,
- use_defaults_for_unserializable_fields=use_defaults_for_unserializable_fields,
- )
- return json.dumps(unstructured, indent=indent, sort_keys=sort_keys)
- except Exception as e:
- raise SerializationError(str(e)) from e
-
-
-def deserialize_from_json(
- data: str,
- for_javascript: bool = False,
- exclude_dont_serialize_fields: bool = False,
- use_defaults_for_unserializable_fields: bool = False,
-) -> Any:
- try:
- converter = CONVERTER_FACTORY.get_converter(
- for_javascript=for_javascript,
- exclude_dont_serialize_fields=exclude_dont_serialize_fields,
- use_defaults_for_unserializable_fields=use_defaults_for_unserializable_fields,
- )
- return _deserialize_serialized_object(json.loads(data), _ShouldDeserialize, converter=converter)
- except Exception as e:
- raise SerializationError(str(e)) from e
-
-
-def deserialize_from_dict(
- data: dict[str, Any],
- as_type: type = _ShouldDeserialize,
- for_javascript: bool = False,
- exclude_dont_serialize_fields: bool = False,
- use_defaults_for_unserializable_fields: bool = False,
-) -> Any:
- try:
- converter = CONVERTER_FACTORY.get_converter(
- for_javascript=for_javascript,
- exclude_dont_serialize_fields=exclude_dont_serialize_fields,
- use_defaults_for_unserializable_fields=use_defaults_for_unserializable_fields,
- )
- return _deserialize_using_type_marker(data, as_type, converter=converter)
- except Exception as e:
- raise SerializationError(str(e)) from e
diff --git a/imbue_core/imbue_core/data_types.py b/imbue_core/imbue_core/data_types.py
@@ -1,317 +0,0 @@
-"""
-Interfaces and data types for the issue identification system.
-
-We foresee two basic kinds of issue identifiers:
-
- 1. Heavily specialized ones.
- - Those would typically only check for a single well-defined issue.
- - The user only wants to know if a specific thing is wrong.
- 2. General ones.
- - They would still have a certain focus but they would typically check for a broader range of issues.
- - We wouldn't know the whole range of possible issues in advance.
- - Here, the user asks for up to N most problematic issues of the given kind in a given scope.
-
-Issue identifiers can either be created:
- - by implementing the IssueIdentifier protocol in an arbitrary way
- - or by leaning on a common LLM-based zero-shot classifier
- - This has the advantage of being more efficient.
- - We can ask for a (set of) score(s) on various metrics / error types for a list of scopes.
- - These computations can basically all be batched together into a single call to the LLM.
- - NOTE: as of writing this, this hasn't been implemented yet.
-
-What follows is a list of possible issues we may eventually want to identify:
-
-
-- docstrings / documentation / tests / constraints / validation
- - missing
- - outdated
- - ambiguous
- - poorly written / duplicated
- - conflicting
- - insufficient
-- assumptions
- - unstated
- - violated
- - conflicting
-- possible race condition
-- overly complex code
-- code in need of refactoring
-- project layout in need of refactoring
-- duplicated code
-- brittle logic
-- use of state (at all, where unnecessary, where needless)
-- gross inefficiency
-- caching (the presence of, at all)
-- bad/confusing/unclear naming
-- forbidden stylistic patterns (that cannot be caught by ratchets)
-- poorly handled edge cases
-- just plain ol bugs, overall correctness, etc
-- missing test coverage (not line based, but more meaning based, esp around integration tests)
-- disagreeing ensembles
-- missing features / implementations / etc
-- refactoring elements that were missed
-- invocation outputs that seem suspect
-- overly broad types
-- misunderstanding the users mental model
-- architectural flaws
-- general critiques
-- better alternatives
-- reinvented wheels / places where some library or external service should have been used
-- mutated globals / global state / imported globals
-- any unnecessary complexity
-
-There are also things we explicitly don't want to catch with this system:
-
-- runtime errors (when running a main script or tests)
-- most ratchet errors
-- anything caught by an existing tool (typing, pylint, tests, etc)
-- errors during the deployment process itself
-- errors when building the image
-- errors about packaging, installation, dependencies, etc
-- errors collected from production
-
-"""
-
-from enum import StrEnum
-from typing import Literal
-
-from pydantic import Field
-
-from imbue_core.common import generate_id
-from imbue_core.pydantic_serialization import SerializableModel
-
-# Define semantics for the normalized confidence and severity scores.
-CONFIDENCE_CERTAINLY_FINE = (0.0, 0.2)
-CONFIDENCE_RATHER_FINE = (0.2, 0.4)
-CONFIDENCE_NOT_SURE = (0.4, 0.6)
-CONFIDENCE_RATHER_PROBLEMATIC = (0.6, 0.8)
-CONFIDENCE_CERTAINLY_PROBLEMATIC = (0.8, 1.0)
-
-
-class ConfidenceScore(SerializableModel):
- """
- A score for the confidence in the issue / error detection.
-
- - The raw score is the score as output by the underlying model.
- - The normalized score is rescaled in such a way that the interval between 0 and 1 maps to the defined confidence levels.
-
- """
-
- raw: float
- normalized: float
-
-
-class SeverityScore(SerializableModel):
- """
- A score for the severity of the issue / error.
-
- - The raw score is the score as potentially output by the underlying model.
- - The normalized score is rescaled in such a way that the interval between 0 and 1 maps to the defined severity levels.
-
- """
-
- raw: float
- normalized: float
-
-
-class LineRange(SerializableModel):
- start: int
- end: int
-
- def __lt__(self, other: "LineRange") -> bool:
- if self.start != other.start:
- return self.start < other.start
- return self.end < other.end
-
- @classmethod
- def build_from_substring(cls, file_contents: str, substring: str) -> tuple["LineRange", ...]:
- """
- Convert a substring in a file to a tuple of LineRange instances.
-
- Each LineRange instance corresponds to a single occurrence of the substring in the file.
- (Except when multiple occurences are on the same line, in which case only one LineRange is
- created to represent them).
-
- LineRanges are returned in the order they appear in the file.
-
- In case the substring can't be found, an empty tuple is returned.
-
- """
-
- line_ranges = set()
- offset_chars = 0
- offset_lines = 0
- while True:
- cut_contents = file_contents[offset_chars:]
- start_index = cut_contents.find(substring)
- if start_index == -1:
- break
- end_index = start_index + len(substring)
- line_start = offset_lines + cut_contents.count("\n", 0, start_index)
- line_end = offset_lines + cut_contents.count("\n", 0, end_index)
- offset_chars += end_index
- offset_lines = line_end
- line_ranges.add(LineRange(start=line_start, end=line_end))
- return tuple(sorted(line_ranges))
-
-
-class AgenticPhase(StrEnum):
- """Phases of agentic analysis."""
-
- ISSUE_IDENTIFICATION = "issue_identification"
- COLLATION = "collation"
- FILTRATION = "filtration"
- DEDUPLICATION = "deduplication"
-
-
-class IssueIdentifierType(StrEnum):
- BATCHED_COMMIT_CHECK = "batched_commit_check"
- CORRECTNESS_COMMIT_CLASSIFIER = "correctness_commit_classifier"
- AGENTIC_ISSUE_IDENTIFIER = "agentic_issue_identifier"
- CONVERSATION_HISTORY_IDENTIFIER = "conversation_history_issue_identifier"
-
-
-class IssueCode(StrEnum):
- """
- A code for the type of issue / error detected.
-
- The code can either correspond something very specific (e.g. "ambiguous_docstring")
- or to something more general (e.g. "function_implementation").
-
- The latter case would be used as an "umbrella" code in cases we don't know what exactly comes out of an issue verifier.
-
- """
-
- # Verifier-based.
- INCORRECT_FUNCTION_IMPLEMENTATION = "incorrect_function_implementation"
-
- # Batched file checks
- INEFFICIENT_CODE = "inefficient_code"
- BAD_NAMING = "bad_naming"
- POOR_DOCSTRING = "poor_docstring"
- RACE_CONDITION = "race_condition"
- HARDCODED_SECRET = "hardcoded_secret"
- DUPLICATE_CODE = "duplicate_code"
- UNUSED_CODE = "unused_code"
- COMMIT_MESSAGE_MISMATCH = "commit_message_mismatch"
-
- # Batched commit checks
- INCOMPLETE_INTEGRATION_WITH_EXISTING_CODE = "incomplete_integration_with_existing_code"
- DOCUMENTATION_IMPLEMENTATION_MISMATCH = "documentation_implementation_mismatch"
- USER_REQUEST_ARTIFACTS_LEFT_IN_CODE = "user_request_artifacts_left_in_code"
- POOR_NAMING = "poor_naming"
- REPETITIVE_OR_DUPLICATE_CODE = "repetitive_or_duplicate_code"
- REFACTORING_NEEDED = "refactoring_needed"
- TEST_COVERAGE = "test_coverage"
- RESOURCE_LEAKAGE = "resource_leakage"
- DEPENDENCY_MANAGEMENT = "dependency_management"
- INSECURE_CODE = "insecure_code"
- CORRECTNESS_SYNTAX_ISSUES = "correctness_syntax_issues"
- FAILS_SILENTLY = "fails_silently"
- INSTRUCTION_FILE_DISOBEYED = "instruction_file_disobeyed"
- ABSTRACTION_VIOLATION = "abstraction_violation"
-
- # Correctness commit classifier
- LOGIC_ERROR = "logic_error"
- RUNTIME_ERROR_RISK = "runtime_error_risk"
- INCORRECT_ALGORITHM = "incorrect_algorithm"
- ERROR_HANDLING_MISSING = "error_handling_missing"
- ASYNC_CORRECTNESS = "async_correctness"
- TYPE_SAFETY_VIOLATION = "type_safety_violation"
-
- # Conversation history identifier
- MISLEADING_BEHAVIOR = "misleading_behavior"
- INSTRUCTION_TO_SAVE = "instruction_to_save"
-
- # Github dataset, not yet implemented in commit checks
- MISMATCHED_CODE_PATTERNS = "mismatched_code_patterns"
-
- # Issue code for flagging suggested improvements or new features, as opposed to actual issues
- SUGGESTED_IMPROVEMENT = "suggested_improvement"
-
- # Catchall
- MISCELLANEOUS = "miscellaneous"
- ALL_CODE_ISSUES = "all_code_issues"
-
- # Deprecated
- _DEPRECATED_LLM_ARTIFACTS_LEFT_IN_CODE = "llm_artifacts_left_in_code"
-
-
-class IssueLocation(SerializableModel):
- """A location in a file."""
-
- line_start: int
- line_end: int
- filename: str | None = None
- # The scope of the issue. Usually the qualified name of the function that the issue is located in.
- # If the issue is part of a class definition (and not limited to a particular method),
- # the name of the class. If the issue is at the global file level, None.
- scope: str | None = None
-
-
-IssueID = str
-
-
-class IdentifiedVerifyIssue(SerializableModel):
- """An identified code issue / error."""
-
- issue_id: IssueID | None = Field(default_factory=generate_id)
- code: IssueCode
- description: str
- severity_score: SeverityScore
- location: tuple[IssueLocation, ...] = Field(default_factory=tuple)
- confidence_score: ConfidenceScore | None = None
- fix: str | None = None
- violating_instruction: str | None = None
- violating_instruction_location: IssueLocation | None = None
- # TODO: remove these fields
-
- # An issue is fundamentally fixable if we can change the implementation to make the issue go away.
- # (An example of a non-fixable issue is a nonsensical commit message - changing the implementation doesn't help here.)
- # - iffv something is not fixable why would we want to report it?
- is_fixable: bool = True
-
-
-class InvocationInfo(SerializableModel):
- """Information about an LLM invocation including token usage, timing, and cost. Populate whichever fields are available."""
-
- input_tokens: int | None = None
- cache_creation_input_tokens: int | None = None
- cache_read_input_tokens: int | None = None
- total_input_tokens: int | None = None
- output_tokens: int | None = None
- duration_ms: float | None = None
- cost: float | None = None
- num_turns: int | None = None
-
-
-class IssueIdentificationLLMResponseMetadata(SerializableModel):
- """Configuration metadata for LLM responses."""
-
- type: Literal["IssueIdentificationLLMResponseMetadata", "IssueIdentificationLLMResponseConfig"] = (
- "IssueIdentificationLLMResponseMetadata"
- )
- agentic_phase: AgenticPhase | None = None
- issue_type: IssueCode | None = None
- identifier_name: str | None = None
- issue_ids: tuple[IssueID] | None = None
-
-
-class LLMResponse(SerializableModel):
- metadata: IssueIdentificationLLMResponseMetadata # Make this a union if there are other types of LLM responses
- raw_response: tuple[str, ...]
- invocation_info: InvocationInfo | None = None
-
- # Deprecated fields
- config: IssueIdentificationLLMResponseMetadata | None = Field(default=None, deprecated=True)
-
-
-class IssueIdentificationDebugInfo(SerializableModel):
- llm_responses: tuple[LLMResponse, ...]
-
-
-class IssueIdentifierResult(SerializableModel):
- """Container for an identified issue along with the LLM responses that generated it."""
-
- issue: IdentifiedVerifyIssue
- passes_filtration: bool = True
diff --git a/imbue_core/imbue_core/itertools.py b/imbue_core/imbue_core/itertools.py
@@ -1,52 +0,0 @@
-import contextlib
-import itertools
-from typing import Generator
-from typing import Iterable
-from typing import Sequence
-from typing import TypeVar
-
-from imbue_core.errors import ImbueError
-
-T = TypeVar("T")
-
-
-class ImbueItertoolsValueError(ImbueError, ValueError):
- """This value error is thrown when the assumptions of the itertools module are violated."""
-
-
-def flatten(iterable: Iterable[Iterable[T]]) -> list[T]:
- return list(itertools.chain.from_iterable(iterable))
-
-
-def remove_none(data: Iterable[T | None]) -> list[T]:
- return [x for x in data if x is not None]
-
-
-def only(x: Iterable[T]) -> T:
- try:
- (value,) = x
- except ValueError as e:
- message = "Expected exactly one value"
- if isinstance(x, Sequence):
- with contextlib.suppress():
- message += f" but got {len(x)} {x[:3]=}"
- raise ImbueItertoolsValueError(message) from e
-
- return value
-
-
-def first(iterable: Iterable[T]) -> T | None:
- return next(iter(iterable), None)
-
-
-# TODO replace with itertools.batched when we can require Python 3.12+
-def generate_chunks(iterable: Iterable[T], chunk_size: int) -> Generator[tuple[T, ...], None, None]:
- """Yield successive n-sized chunks from any iterable"""
- chunk = []
- for item in iterable:
- chunk.append(item)
- if len(chunk) == chunk_size:
- yield tuple(chunk)
- chunk = []
- if len(chunk) > 0:
- yield tuple(chunk)
diff --git a/imbue_core/imbue_core/nested_evolver.py b/imbue_core/imbue_core/nested_evolver.py
@@ -1,220 +0,0 @@
-"""Evolver uses duck-typing to give the appearance of editing a frozen, nested structure of attrs classes and tuples, recording changes in a way that they can be applied to generate a newly frozen instance.
-
-One of the design goals is that mypy, autocomplete, and automatic refactoring work for the assignments made into these nested structures.
-
-If you make changes here and then the tests fail with:
-```
-E RecursionError: maximum recursion depth exceeded
-!!! Recursion detected (same locals & position)
-```
-It's possible that you're accidentally invoking `Evolver.something_undefined` and that's causing the infinite recursion.
-Mypy cannot catch this when it thinks the type is an `Evolver` because the `Evolver` class has a `__getattr__` method that makes it look like any attribute access could be valid.
-"""
-
-import threading
-from typing import Any
-from typing import Callable
-from typing import Generic
-from typing import TypeVar
-from typing import cast
-
-import attr
-from pydantic import BaseModel
-
-from imbue_core.frozen_utils import FrozenDict
-from imbue_core.pydantic_utils import model_update
-
-_T = TypeVar("_T")
-
-_threading_local = threading.local()
-
-
-def evolver(obj: _T) -> _T:
- """Creates a wrapper around an immutable attrs object, tuple, or FrozenDict that records potentially nested attribute assignments.
-
- The return type is our first white lie to the type system.
- """
- result = _Evolver[_T](obj)
- # The cast is a little white lie to the type system to make type-checking, autocomplete, and refactoring work.
- return cast(_T, result)
-
-
-def assign(dest: _T, src: Callable[[], _T]) -> None:
- """Since mypy would complain about assignments to frozen attrs fields, use this function to make assignments.
-
- The only reason src is a `Callable[[], _T]` instead of just `_T` is that it makes type checking signal attempts to assign
- the wrong type to the field. Surprisingly, just using `(dest: _T, src: _T)` doesn't cause mypy to complain about type mismatch.
- """
- assert isinstance(dest, _Evolver) # Tricked you, type system!
- dest_evolver: _Evolver[_T] = cast(_Evolver[_T], dest)
- dest_evolver.assign(src())
-
-
-def chill(evolver: _T) -> _T:
- """Produces a new frozen instance with the recorded changes applied.
-
- The name `chill` is a play on the fact that original input was frozen, and we are now re-freezing it.
- """
- assert isinstance(evolver, _Evolver) # Tricked you, type system!
- cast_evolver = cast(_Evolver[_T], evolver)
- return cast_evolver.chill()
-
-
-class _RegularValue:
- regular_value: Any
-
- def __init__(self, value: Any) -> None:
- self.regular_value = value
-
-
-class _AttrValue:
- attr_value: Any
- child_evolver_by_name: dict[str, "_Evolver[Any]"]
-
- def __init__(self, value: Any) -> None:
- self.attr_value = value
- self.child_evolver_by_name = {}
-
-
-class _PydanticModelValue:
- pydantic_model_value: Any
- child_evolver_by_name: dict[str, "_Evolver[Any]"]
-
- def __init__(self, value: Any) -> None:
- self.pydantic_model_value = value
- self.child_evolver_by_name = {}
-
-
-class _TupleValue:
- tuple_evolvers: list["_Evolver[Any]"]
-
- def __init__(self, value: tuple[Any, ...]) -> None:
- # It may be premature to create evolvers for all the elements of the tuple, but it's easier.
- self.tuple_evolvers = [evolver(item) for item in value]
-
-
-class _FrozenDictValue:
- frozen_dict_evolvers: dict[Any, "_Evolver[Any]"]
-
- def __init__(self, value: dict[Any, Any]) -> None:
- # It may be premature to create evolvers for all the elements of dict, but it's easier.
- self.frozen_dict_evolvers = {k: evolver(v) for k, v in value.items()}
-
-
-class _Evolver(Generic[_T]):
- # pyre-ignore[13]: pyre is confused by the trickery here
- _value: (
- _RegularValue
- | _AttrValue
- | _TupleValue
- | _FrozenDictValue
- | _PydanticModelValue
- )
-
- def __init__(self, initial_value: _T) -> None:
- super().__init__()
- self.assign(initial_value)
-
- def assign(self, new_value: _T) -> None:
- """Assign a new value to this Evolver, recording a change to the frozen structure to be later applied during `chill()`."""
-
- if attr.has(type(new_value)):
- self._value = _AttrValue(new_value)
- elif isinstance(new_value, BaseModel):
- self._value = _PydanticModelValue(new_value)
- elif isinstance(new_value, tuple):
- self._value = _TupleValue(new_value)
- elif isinstance(new_value, FrozenDict):
- self._value = _FrozenDictValue(new_value)
- else:
- self._value = _RegularValue(new_value)
-
- def __getattr__(self, item: str) -> "_Evolver[Any]":
- """Access Evolvers for nested members of a frozen attrs object."""
- try:
- value = self._value
- if isinstance(value, _AttrValue):
- if item not in value.child_evolver_by_name:
- child_obj = getattr(value.attr_value, item)
- result = evolver(child_obj)
- assert isinstance(result, _Evolver), (
- "Expose a lie to the type system."
- )
- value.child_evolver_by_name[item] = result
- return value.child_evolver_by_name[item]
- elif isinstance(value, _PydanticModelValue):
- if item not in value.child_evolver_by_name:
- child_obj = getattr(value.pydantic_model_value, item)
- result = evolver(child_obj)
- assert isinstance(result, _Evolver), (
- "Expose a lie to the type system."
- )
- value.child_evolver_by_name[item] = result
- return value.child_evolver_by_name[item]
- raise TypeError(
- f"You're trying to access field {item=} on an object of {type(value)=} that doesn't have that field (should have been a mypy error)."
- )
- except BaseException as e:
- if hasattr(_threading_local, "evolved_obj"):
- # pyre-ignore[16]: pyre is suspicious of the trickery here
- if getattr(_threading_local, "evolved_obj") == self:
- delattr(_threading_local, "evolved_obj")
- raise e
-
- # TODO: It wouldn't be terribly difficult to support "appending" to the tuple as well, by appending to this list.
- def __getitem__(self, key: Any) -> "_Evolver[Any]":
- """Access Evolvers for the elements of a tuple or dict."""
- value = self._value
- if isinstance(value, _TupleValue):
- assert isinstance(key, int)
- return value.tuple_evolvers[key]
- elif isinstance(value, _FrozenDictValue):
- if key not in value.frozen_dict_evolvers:
- # Presumably we're going to evolver_assign to this very soon.
- cast(_FrozenDictValue, self._value).frozen_dict_evolvers[key] = (
- _Evolver(_RegularValue(None))
- )
- return cast(_FrozenDictValue, self._value).frozen_dict_evolvers[key]
- raise TypeError(
- f"You're using [square_brackets] access {key=} on an object of {type(self._value)=} that doesn't support this (should have been a mypy error)."
- )
-
- def chill(self) -> _T:
- """Recursively apply the recorded changes to the original object and return a new frozen instance."""
- if isinstance(self._value, _AttrValue):
- new_children: dict[str, Any] = {
- name: chill(child)
- for name, child in self._value.child_evolver_by_name.items()
- }
- assert attr.has(self._value.attr_value.__class__)
- return cast(
- _T,
- attr.evolve(
- cast(Any, cast(_AttrValue, self._value).attr_value), **new_children
- ),
- )
- elif isinstance(self._value, _PydanticModelValue):
- return cast(
- _T,
- model_update(
- self._value.pydantic_model_value,
- update={
- name: chill(child)
- for name, child in self._value.child_evolver_by_name.items()
- },
- ),
- )
- elif isinstance(self._value, _TupleValue):
- return cast(
- _T, tuple(evolver.chill() for evolver in self._value.tuple_evolvers)
- )
- elif isinstance(self._value, _RegularValue):
- return cast(_T, self._value.regular_value)
- elif isinstance(self._value, _FrozenDictValue):
- return cast(
- _T,
- FrozenDict(
- {k: v.chill() for k, v in self._value.frozen_dict_evolvers.items()}
- ),
- )
- raise ValueError(f"This Evolver has no value to evolve, {type(self._value)=}.")
diff --git a/imbue_core/imbue_core/pydantic_serialization.py b/imbue_core/imbue_core/pydantic_serialization.py
@@ -1,118 +0,0 @@
-import threading
-from typing import Any
-from typing import TypeVar
-from typing import cast
-
-from pydantic import BaseModel
-from pydantic import ConfigDict
-from pydantic import Discriminator
-from pydantic.alias_generators import to_camel
-
-from imbue_core.nested_evolver import _Evolver
-from imbue_core.nested_evolver import chill
-from imbue_core.nested_evolver import evolver
-from imbue_core.serialization_types import Serializable
-
-T = TypeVar("T", bound=BaseModel)
-V = TypeVar("V")
-
-_threading_local = threading.local()
-
-
-class EvolvableModel:
- # pyre-ignore[47]: pyre is not so easily tricked
- def evolve(self: T, attribute: V, new_value: V) -> T:
- # pyre-ignore[16]: pyre doesn't know about evolved_obj
- assert _threading_local.evolved_obj is not None, (
- ".ref() must be called before evolve"
- )
-
- assert isinstance(attribute, _Evolver) # Tricked you, type system!
- dest_evolver: _Evolver[T] = cast(_Evolver[T], attribute)
- dest_evolver.assign(new_value)
-
- result = chill(_threading_local.evolved_obj)
- _threading_local.evolved_obj = None
- return result
-
- # pyre-ignore[47]: pyre is not so easily tricked
- def ref(self: T) -> T:
- # pyre-ignore[16]: pyre doesn't know about evolved_obj
- _threading_local.evolved_obj = evolver(self)
- return _threading_local.evolved_obj
-
-
-class MutableModel(BaseModel):
- """
- The base class for any internal data that strictly must be mutable. Should be used sparingly.
- """
-
- model_config = ConfigDict(
- frozen=False,
- extra="forbid",
- # FIXME: go back to preventing arbitrary types once we're done converting
- # arbitrary_types_allowed=False,
- arbitrary_types_allowed=True,
- )
-
-
-class SerializableModel(EvolvableModel, BaseModel, Serializable):
- """
- The base class for all data that can be serialized to/from JSON.
- """
-
- model_config = ConfigDict(
- frozen=True,
- ser_json_bytes="base64",
- val_json_bytes="base64",
- alias_generator=to_camel,
- validate_by_alias=True,
- validate_by_name=True,
- # any extra values will end up in the __pydantic_extra__ field
- # this is effectively required for backwards compatibility
- # IMPORTANT: note that, by default, we clear this below! These types are ONLY for backwards compatibility
- extra="allow",
- # this is also effectively required for backwards compatibility
- arbitrary_types_allowed=True,
- )
-
- # this is a place where we might way to do any backwards compatibility related logic
- def model_post_init(self, __context: Any) -> None:
- pydantic_extra = self.__pydantic_extra__
- assert pydantic_extra is not None
- pydantic_extra.clear()
-
-
-# this is mostly here for the default cases.
-# When you want to upgrade a model (and keep it backwards compatible), you can make a custom discriminator
-# (eg, that looks for the old type name or converts the old class names)
-def build_discriminator(
- field_name: str = "object_type",
- additional_types_and_string_representations: tuple[tuple[type, str], ...] = (),
-) -> Discriminator:
- """
- Build a discriminator function for a Pydantic model.
-
- Args:
- field_name (str): The name of the field to use as the discriminator.
- additional_types_and_string_representations (Tuple[Tuple[Type, str], ...]): Register additional types to the discriminator.
-
- Returns:
- Callable[[T | dict], str]: A function that takes an instance of T or a dictionary and returns the value of the
- specified field.
- """
-
- def discriminator(obj: T | dict) -> str:
- for (
- model_type,
- string_representation,
- ) in additional_types_and_string_representations:
- if isinstance(obj, model_type):
- return string_representation
- if isinstance(obj, dict):
- if field_name not in obj:
- return obj[to_camel(field_name)]
- return obj[field_name]
- return getattr(obj, field_name)
-
- return Discriminator(discriminator=discriminator)
diff --git a/imbue_core/imbue_core/serialization.py b/imbue_core/imbue_core/serialization.py
@@ -1,424 +0,0 @@
-import builtins
-import datetime
-import json
-from enum import Enum
-from functools import cached_property
-from importlib import import_module
-from importlib.metadata import version
-from pathlib import PosixPath
-from traceback import format_tb
-from types import TracebackType
-from typing import Any
-from typing import Hashable
-from typing import Iterable
-from typing import Mapping
-from typing import TypeVar
-from typing import cast
-from uuid import UUID
-
-from loguru import logger
-from typing_extensions import TypeAliasType
-from yasoo import Deserializer
-from yasoo import Serializer
-from yasoo.constants import ENUM_VALUE_KEY
-from yasoo.objects import DictWithSerializedKeys
-from yasoo.serialization import _convert_to_json_serializable
-from yasoo.utils import get_fields
-from yasoo.utils import is_obj_supported_primitive
-from yasoo.utils import normalize_type
-from yasoo.utils import resolve_types
-
-from imbue_core.fixed_traceback import FixedTraceback
-from imbue_core.pydantic_serialization import SerializableModel
-from imbue_core.serialization_types import Serializable
-
-assert version("yasoo") == "0.12.6", (
- "This code was written for yasoo 0.12.6 and requires inheriting / monkeypatching the deserializer, so you probably don't want to use any other version without fixing TupleDeserializer"
-)
-
-T = TypeVar("T", bound=Hashable)
-
-
-class TupleDeserializer(Deserializer):
- def _deserialize(
- self,
- data: bool | int | float | str | list[Any] | dict[str, Any] | None,
- obj_type: type[T] | None,
- type_key: str | None,
- allow_extra_fields: bool,
- external_globals: dict[str, Any],
- ignore_custom_deserializer: bool = False,
- ) -> object:
- all_globals = dict(globals())
- all_globals.update(external_globals)
- if is_obj_supported_primitive(data):
- return data
- if isinstance(data, list):
- list_types = self._get_list_types(obj_type, data)
- return tuple(
- [
- self._deserialize(d, t, type_key, allow_extra_fields, all_globals)
- for t, d in list_types
- ]
- )
-
- assert isinstance(data, dict), f"Expected a dict, but got {type(data)}"
-
- # load wrapped primitives
- if type_key is not None:
- type_data = data.get(type_key, None)
-
- if (
- type_data is not None
- and type_data.startswith("builtins.")
- and type_data != "builtins.dict"
- ):
- return data["value"]
-
- # TODO: we need to potentially handle `builtins.dict`
- # if type_key is not None:
- # type_data = data.get(type_key, None)
- #
- # # TODO: serialization currently breaks with builtin.dicts and dicts with non-string keys
- # if type_data == "builtins.dict":
- # raise NotImplementedError(
- # "Only `FrozenMapping` is supported for dict serialization/deserialization, call `freeze_mapping` on your dict before serializing"
- # )
- # if type_data is not None and type_data.startswith("builtins.") and type_data != "builtins.dict":
- # return data["value"]
-
- # TODO: remove this hack. Many of our sqlite files (search s3_sqlite_path) have FrozenDicts
- if (
- isinstance(type_key, str)
- and data.get(type_key, None) == "flax.core.frozen_dict.FrozenDict"
- ):
- data[type_key] = "imbue_core.frozen_utils.FrozenMapping"
- # we deliberately pass in a `None` type_key sometimes, which results in just returning obj_type
- obj_type = self._get_object_type(
- obj_type, data, type_key, all_globals
- ) # pyre-ignore[6]
- if type_key in data:
- data.pop(type_key)
- real_type, generic_args = normalize_type(obj_type, all_globals)
- if external_globals and isinstance(real_type, type):
- bases = {real_type}
- while bases:
- all_globals.update((b.__name__, b) for b in bases)
- bases = {ancestor for b in bases for ancestor in b.__bases__}
-
- if not ignore_custom_deserializer:
- deserialization_method = self._custom_deserializers.get(
- obj_type, self._custom_deserializers.get(real_type)
- )
- if deserialization_method:
- return deserialization_method(data)
- for base_class, method in self._inheritance_deserializers.items():
- if issubclass(real_type, base_class):
- return method(data, real_type)
-
- key_type = None
- try:
- # pyre-fixme[6]: obj_type needs to be Hashable, but pyre isn't sure that it is
- fields = {f.name: f for f in get_fields(obj_type)}
- except TypeError:
- if obj_type is FixedTraceback:
- return FixedTraceback.from_dict(data["value"])
- if issubclass(real_type, Enum):
- value = data[ENUM_VALUE_KEY]
- if isinstance(value, str):
- try:
- return real_type[value]
- except KeyError:
- for e in real_type:
- if e.name.lower() == value.lower():
- return e
- return real_type(value)
- # TODO: serialization currently breaks with builtin.dicts and dicts with non-string keys
- # if you have weird keys in your dict this branch won't be hit and your object won't be properly deserialized
- elif issubclass(real_type, Mapping):
- key_type = generic_args[0] if generic_args else None
- if self._is_mapping_dict_with_serialized_keys(key_type, data):
- # pyre-fixme[9]: obj_type needs to be Hashable, but pyre doesn't realize that type[DictWithSerializedKeys] is ok
- obj_type = DictWithSerializedKeys
- # pyre-fixme[6]: arg of get_fields needs to be Hashable, but pyre doesn't realize that type[DictWithSerializedKeys] is ok
- fields = {f.name: f for f in get_fields(DictWithSerializedKeys)}
- value_type = generic_args[1] if generic_args else Any
- fields["data"].field_type = dict[str, value_type] # type: ignore
- else:
- return self._load_mapping(
- data,
- real_type,
- generic_args,
- type_key,
- allow_extra_fields,
- all_globals,
- )
- elif issubclass(real_type, Iterable):
- # If we got here it means data is not a list, so obj_type came from the data itself and is safe to use
- return self._load_iterable(
- data, obj_type, type_key, allow_extra_fields, all_globals
- )
- elif real_type != obj_type:
- return self._deserialize(
- data, real_type, type_key, allow_extra_fields, external_globals
- )
- else:
- raise
-
- self._check_for_missing_fields(data, fields, obj_type)
- self._check_for_extraneous_fields(data, fields, obj_type, allow_extra_fields)
- self._load_inner_fields(data, fields, type_key, allow_extra_fields, all_globals)
- if obj_type is DictWithSerializedKeys:
- return self._load_dict_with_serialized_keys(
- obj_type(**data), key_type, type_key, allow_extra_fields, all_globals
- )
- kwargs = {k: v for k, v in data.items() if fields[k].init}
- assert obj_type is not None
- result = obj_type(**kwargs)
- for k, v in data.items():
- if k not in kwargs:
- setattr(result, k, v)
- return result
-
-
-# TODO: probably a good idea to ensure that all dicts are frozen as well...
-class FrozenSerializer(Serializer):
- def __init__(
- self, force_serialization: bool, allow_unsafe_list_serialization: bool = False
- ) -> None:
- super().__init__()
- self._force_serialization = force_serialization
- self._allow_unsafe_list_serialization = allow_unsafe_list_serialization
-
- def _serialize_iterable(
- self,
- obj: Iterable[object],
- type_key: Any,
- fully_qualified_types: Any,
- preserve_iterable_types: Any,
- stringify_dict_keys: Any,
- ) -> list[object]:
- if isinstance(obj, list):
- if self._allow_unsafe_list_serialization:
- logger.info("Converting list to tuple for serialization: {}", obj)
- obj = tuple(obj)
- else:
- raise Exception(
- f"Lists are not allowed for serialization. Use tuples instead. Current iterable: {obj}"
- )
- assert isinstance(obj, (tuple, frozenset, bytes)), (
- f"All iterables should be tuples or frozenset. Received {obj}"
- )
- return cast(
- list[object],
- tuple(
- self._serialize(
- item,
- type_key,
- fully_qualified_types,
- preserve_iterable_types,
- stringify_dict_keys,
- )
- for item in obj
- ),
- )
-
- # overriding this method just to get some better error messages out--previously it would just "type error" and
- # moan about things like int64 not being serializable, which is fine, but it is nicer if the key is included
- def serialize(
- self,
- obj: Any,
- type_key: str | None = "__type",
- fully_qualified_types: bool = True,
- preserve_iterable_types: bool = False,
- stringify_dict_keys: bool = True,
- globals: dict[str, Any] | None = None,
- ) -> bool | int | float | str | list | dict[str, Any] | None:
- try:
- if is_obj_supported_primitive(obj):
- return obj # type: ignore
-
- if globals:
- self._custom_serializers = resolve_types(
- self._custom_serializers, globals
- ) # type: ignore
-
- result = self._serialize(
- obj,
- type_key,
- fully_qualified_types,
- preserve_iterable_types,
- stringify_dict_keys,
- inner=False,
- )
- try:
- result = _convert_to_json_serializable(result)
- except TypeError:
- _convert_to_json_serializable_with_better_errors(result)
- assert False, "previous method should have raised..."
- return result # type: ignore
- except Exception:
- if self._force_serialization:
- return repr(obj)
- else:
- raise
-
-
-JsonTypeAlias = TypeAliasType(
- "JsonTypeAlias",
- "dict[str, JsonTypeAlias] | list[JsonTypeAlias] | str | int | float | bool | None",
-)
-
-
-class SerializedException(SerializableModel):
- """A serializable dataclass that represents an exception"""
-
- exception: str
- args: "tuple[SerializedException | JsonTypeAlias, ...]" # pyre-ignore[11]: pyre doesn't like TypeAliasType
- traceback_dict: JsonTypeAlias
-
- @classmethod
- def build(
- cls, exception: BaseException, traceback: TracebackType | None = None
- ) -> "SerializedException":
- if traceback is None:
- traceback = exception.__traceback__
- assert traceback is not None, " ".join(
- (
- "No traceback deriveable or as a concrete argument!",
- f"You probably want to convert_to_serialized_exception in your except clause: {exception=}",
- )
- )
- return SerializedException( # pyre-fixme[28]: pyre doesn't understand pydantic
- exception=get_fully_qualified_name_for_error(exception),
- args=tuple(
- _convert_serialized_exception_args(x, traceback) for x in exception.args
- ),
- traceback_dict=FixedTraceback.from_tb(traceback).as_dict(),
- )
-
-
-def _convert_serialized_exception_args(
- error: Serializable, traceback: TracebackType | None = None
-) -> JsonTypeAlias:
- if isinstance(error, BaseException):
- return SerializedException.build(error, traceback=traceback)
- elif isinstance(error, (list, tuple)):
- return tuple(_convert_serialized_exception_args(x, traceback) for x in error)
- return error
-
-
-def get_fully_qualified_name_for_error(e: BaseException) -> str:
- if e.__class__.__module__ == "builtins":
- return e.__class__.__name__
- return f"{e.__class__.__module__}.{e.__class__.__name__}"
-
-
-def _convert_to_json_serializable_with_better_errors(
- obj: Any, path: str = ""
-) -> int | float | str | list | dict | None:
- if is_obj_supported_primitive(obj):
- return obj # type: ignore
- if isinstance(obj, Mapping):
- return {
- key: _convert_to_json_serializable_with_better_errors(
- value, f"{path}.{key}"
- )
- for key, value in obj.items()
- }
- if isinstance(obj, Iterable):
- return [
- _convert_to_json_serializable_with_better_errors(item, f"{path}[{i}]")
- for i, item in enumerate(obj)
- ]
- raise TypeError(
- f'Found object of type "{type(obj).__name__}" at {path} which cannot be serialized'
- )
-
-
-SERIALIZER = FrozenSerializer(
- force_serialization=False, allow_unsafe_list_serialization=False
-)
-DESERIALIZER = TupleDeserializer()
-
-# note: you cannot change this without changing other calls to yasoo, this is its default
-TYPE_KEY = "__type"
-
-
-class SerializationError(Exception):
- pass
-
-
-@SERIALIZER.register()
-def serialize_frozen_set(data: frozenset) -> dict:
- value = SERIALIZER.serialize(tuple(data))
- return {"value": value}
-
-
-@DESERIALIZER.register()
-def deserialize_frozen_set(data: dict) -> frozenset:
- return frozenset(DESERIALIZER.deserialize(data["value"], tuple))
-
-
-@SERIALIZER.register()
-def serialize_uuid(data: UUID) -> dict:
- return {"value": data.hex}
-
-
-@DESERIALIZER.register()
-def deserialize_uuid(data: dict) -> UUID:
- return UUID(data["value"])
-
-
-@SERIALIZER.register()
-def serialize_traceback(data: FixedTraceback) -> dict:
- return {"value": data.to_dict()}
-
-
-@DESERIALIZER.register()
-def deserialize_traceback(data: dict) -> FixedTraceback:
- return FixedTraceback.from_dict(data["value"])
-
-
-@SERIALIZER.register()
-def serialize_posix_path(data: PosixPath) -> dict:
- return {"value": str(data)}
-
-
-@DESERIALIZER.register()
-def deserialize_posix_path(data: dict) -> PosixPath:
- return PosixPath(data["value"])
-
-
-@SERIALIZER.register()
-def serialize_datetime(data: datetime.datetime) -> dict:
- return {
- "time": data.astimezone(datetime.timezone.utc).timestamp(),
- "tzaware": data.tzinfo is not None,
- "__type": "datetime.datetime",
- }
-
-
-@DESERIALIZER.register()
-def deserialize_datetime(data: dict) -> datetime.datetime:
- return datetime.datetime.fromtimestamp(
- data["time"], datetime.timezone.utc if data.get("tzaware", None) else None
- )
-
-
-def serialize_to_json(
- obj: Any, indent: int | None = None, sort_keys: bool = False
-) -> str:
- try:
- return json.dumps(SERIALIZER.serialize(obj), indent=indent, sort_keys=sort_keys)
- except Exception as e:
- raise SerializationError(str(e)) from e
-
-
-def deserialize_from_json(data: str) -> Any:
- try:
- return DESERIALIZER.deserialize(
- json.loads(data)
- ) # pyre-ignore[20]: pyre doesn't understand deserialize
- except Exception as e:
- raise SerializationError(str(e)) from e
diff --git a/imbue_core/pyproject.toml b/imbue_core/pyproject.toml
@@ -1,66 +0,0 @@
-[build-system]
-requires = ["setuptools", "wheel"]
-build-backend = "setuptools.build_meta"
-
-[project]
-name = "imbue-core"
-version = "0.0.9"
-description = "Utilities for imbue-desktop."
-readme = "README.md"
-authors = [
- { name="Imbue", email="imbue@imbue.com" },
-]
-license = "MIT"
-dependencies = [
- "anyio",
- "attrs",
- "boto3>=1.38.27",
- "cachetools",
- "cattrs",
- "diskcache>=5.6.3",
- "grpclib>=0.4.7",
- "httpx",
- "inline-snapshot",
- "loguru",
- "pathspec",
- "prometheus-client>=0.20.0",
- "pydantic-settings",
- "pydantic>=2.11.4",
- "pygit2>=1.18.0",
- "pylint==3.2.6",
- "pygments>=2.0.0",
- "pyhumps",
- "pytest",
- "pytest-asyncio",
- "pytest-mock",
- "syrupy",
- "python-gitlab>=4.5.0",
- "tblib==2.0.0", # pinning because Hammers code relies on "get_locals"
- "tenacity>=8.2.2",
- "toml",
- "traceback-with-variables>=2.2.0",
- "typeid-python",
- "yasoo",
-
- "anthropic~=0.54", # forcing a newer version that is not reliant on old httpx
- "openai>=1.79.0",
- "tiktoken",
- "together",
- "groq>=0.18.0",
- "google-genai>=1.26.0",
-]
-requires-python = ">=3.11,<3.13"
-
-[project.urls]
-"homepage" = "https://imbue.com/"
-
-[tool.setuptools.package-data]
-"imbue" = ["py.typed"]
-
-[tool.setuptools.packages.find]
-include = ["imbue_core*"]
-
-[dependency-groups]
-dev = [
- "moto>=4.1.12",
-]
diff --git a/imbue_core/uv.lock b/imbue_core/uv.lock
@@ -1,1981 +0,0 @@
-version = 1
-revision = 3
-requires-python = ">=3.11, <3.13"
-resolution-markers = [
- "python_full_version >= '3.12'",
- "python_full_version < '3.12'",
-]
-
-[[package]]
-name = "aiohappyeyeballs"
-version = "2.6.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" },
-]
-
-[[package]]
-name = "aiohttp"
-version = "3.13.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "aiohappyeyeballs" },
- { name = "aiosignal" },
- { name = "attrs" },
- { name = "frozenlist" },
- { name = "multidict" },
- { name = "propcache" },
- { name = "yarl" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" },
- { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" },
- { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" },
- { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" },
- { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" },
- { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" },
- { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" },
- { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" },
- { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" },
- { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" },
- { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" },
- { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" },
- { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" },
- { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" },
- { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" },
- { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701, upload-time = "2026-01-03T17:30:10.869Z" },
- { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678, upload-time = "2026-01-03T17:30:12.719Z" },
- { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" },
- { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" },
- { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" },
- { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" },
- { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" },
- { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" },
- { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" },
- { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" },
- { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" },
- { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" },
- { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" },
- { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" },
- { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" },
- { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" },
- { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" },
- { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" },
- { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" },
-]
-
-[[package]]
-name = "aiosignal"
-version = "1.4.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "frozenlist" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
-]
-
-[[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.76.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/6e/be/d11abafaa15d6304826438170f7574d750218f49a106c54424a40cef4494/anthropic-0.76.0.tar.gz", hash = "sha256:e0cae6a368986d5cf6df743dfbb1b9519e6a9eee9c6c942ad8121c0b34416ffe", size = 495483, upload-time = "2026-01-13T18:41:14.908Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e5/70/7b0fd9c1a738f59d3babe2b4212031c34ab7d0fda4ffef15b58a55c5bcea/anthropic-0.76.0-py3-none-any.whl", hash = "sha256:81efa3113901192af2f0fe977d3ec73fdadb1e691586306c4256cd6d5ccc331c", size = 390309, upload-time = "2026-01-13T18:41:13.483Z" },
-]
-
-[[package]]
-name = "anyio"
-version = "4.12.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "idna" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
-]
-
-[[package]]
-name = "astroid"
-version = "3.2.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/9e/53/1067e1113ecaf58312357f2cd93063674924119d80d173adc3f6f2387aa2/astroid-3.2.4.tar.gz", hash = "sha256:0e14202810b30da1b735827f78f5157be2bbd4a7a59b7707ca0bfc2fb4c0063a", size = 397576, upload-time = "2024-07-20T12:57:43.26Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/80/96/b32bbbb46170a1c8b8b1f28c794202e25cfe743565e9d3469b8eb1e0cc05/astroid-3.2.4-py3-none-any.whl", hash = "sha256:413658a61eeca6202a59231abb473f932038fbcbf1666587f66d482083413a25", size = 276348, upload-time = "2024-07-20T12:57:40.886Z" },
-]
-
-[[package]]
-name = "asttokens"
-version = "3.0.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" },
-]
-
-[[package]]
-name = "attrs"
-version = "25.4.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
-]
-
-[[package]]
-name = "backoff"
-version = "2.2.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" },
-]
-
-[[package]]
-name = "black"
-version = "25.12.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "click" },
- { name = "mypy-extensions" },
- { name = "packaging" },
- { name = "pathspec" },
- { name = "platformdirs" },
- { name = "pytokens" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/c4/d9/07b458a3f1c525ac392b5edc6b191ff140b596f9d77092429417a54e249d/black-25.12.0.tar.gz", hash = "sha256:8d3dd9cea14bff7ddc0eb243c811cdb1a011ebb4800a5f0335a01a68654796a7", size = 659264, upload-time = "2025-12-08T01:40:52.501Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/60/ad/7ac0d0e1e0612788dbc48e62aef8a8e8feffac7eb3d787db4e43b8462fa8/black-25.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0cfa263e85caea2cff57d8f917f9f51adae8e20b610e2b23de35b5b11ce691a", size = 1877003, upload-time = "2025-12-08T01:43:29.967Z" },
- { url = "https://files.pythonhosted.org/packages/e8/dd/a237e9f565f3617a88b49284b59cbca2a4f56ebe68676c1aad0ce36a54a7/black-25.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a2f578ae20c19c50a382286ba78bfbeafdf788579b053d8e4980afb079ab9be", size = 1712639, upload-time = "2025-12-08T01:52:46.756Z" },
- { url = "https://files.pythonhosted.org/packages/12/80/e187079df1ea4c12a0c63282ddd8b81d5107db6d642f7d7b75a6bcd6fc21/black-25.12.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e1b65634b0e471d07ff86ec338819e2ef860689859ef4501ab7ac290431f9b", size = 1758143, upload-time = "2025-12-08T01:45:29.137Z" },
- { url = "https://files.pythonhosted.org/packages/93/b5/3096ccee4f29dc2c3aac57274326c4d2d929a77e629f695f544e159bfae4/black-25.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a3fa71e3b8dd9f7c6ac4d818345237dfb4175ed3bf37cd5a581dbc4c034f1ec5", size = 1420698, upload-time = "2025-12-08T01:45:53.379Z" },
- { url = "https://files.pythonhosted.org/packages/7e/39/f81c0ffbc25ffbe61c7d0385bf277e62ffc3e52f5ee668d7369d9854fadf/black-25.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:51e267458f7e650afed8445dc7edb3187143003d52a1b710c7321aef22aa9655", size = 1229317, upload-time = "2025-12-08T01:46:35.606Z" },
- { url = "https://files.pythonhosted.org/packages/d1/bd/26083f805115db17fda9877b3c7321d08c647df39d0df4c4ca8f8450593e/black-25.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31f96b7c98c1ddaeb07dc0f56c652e25bdedaac76d5b68a059d998b57c55594a", size = 1924178, upload-time = "2025-12-08T01:49:51.048Z" },
- { url = "https://files.pythonhosted.org/packages/89/6b/ea00d6651561e2bdd9231c4177f4f2ae19cc13a0b0574f47602a7519b6ca/black-25.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05dd459a19e218078a1f98178c13f861fe6a9a5f88fc969ca4d9b49eb1809783", size = 1742643, upload-time = "2025-12-08T01:49:59.09Z" },
- { url = "https://files.pythonhosted.org/packages/6d/f3/360fa4182e36e9875fabcf3a9717db9d27a8d11870f21cff97725c54f35b/black-25.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1f68c5eff61f226934be6b5b80296cf6939e5d2f0c2f7d543ea08b204bfaf59", size = 1800158, upload-time = "2025-12-08T01:44:27.301Z" },
- { url = "https://files.pythonhosted.org/packages/f8/08/2c64830cb6616278067e040acca21d4f79727b23077633953081c9445d61/black-25.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:274f940c147ddab4442d316b27f9e332ca586d39c85ecf59ebdea82cc9ee8892", size = 1426197, upload-time = "2025-12-08T01:45:51.198Z" },
- { url = "https://files.pythonhosted.org/packages/d4/60/a93f55fd9b9816b7432cf6842f0e3000fdd5b7869492a04b9011a133ee37/black-25.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:169506ba91ef21e2e0591563deda7f00030cb466e747c4b09cb0a9dae5db2f43", size = 1237266, upload-time = "2025-12-08T01:45:10.556Z" },
- { url = "https://files.pythonhosted.org/packages/68/11/21331aed19145a952ad28fca2756a1433ee9308079bd03bd898e903a2e53/black-25.12.0-py3-none-any.whl", hash = "sha256:48ceb36c16dbc84062740049eef990bb2ce07598272e673c17d1a7720c71c828", size = 206191, upload-time = "2025-12-08T01:40:50.963Z" },
-]
-
-[[package]]
-name = "boto3"
-version = "1.42.37"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "botocore" },
- { name = "jmespath" },
- { name = "s3transfer" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/a9/ef/0d6ceb88ae2b3638b956190a431e4a8a3697d5769d4bbbede8efcccacaea/boto3-1.42.37.tar.gz", hash = "sha256:d8b6c52c86f3bf04f71a5a53e7fb4d1527592afebffa5170cf3ef7d70966e610", size = 112830, upload-time = "2026-01-28T20:38:43.339Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/fb/a4/cd334f74498acc6ad42a69c48e8c495f6f721d8abe13f8ef0d4b862fb1c0/boto3-1.42.37-py3-none-any.whl", hash = "sha256:e1e38fd178ffc66cfbe9cb6838b8c460000c3eb741e5f40f57eb730780ef0ed4", size = 140604, upload-time = "2026-01-28T20:38:42.135Z" },
-]
-
-[[package]]
-name = "botocore"
-version = "1.42.37"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "jmespath" },
- { name = "python-dateutil" },
- { name = "urllib3" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/d5/4d/94292e7686e64d2ede8dae7102bbb11a1474e407c830de4192f2518e6cff/botocore-1.42.37.tar.gz", hash = "sha256:3ec58eb98b0857f67a2ae6aa3ded51597e7335f7640be654e0e86da4f173b5b2", size = 14914621, upload-time = "2026-01-28T20:38:34.586Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/72/30/54042dd3ad8161964f8f47aa418785079bd8d2f17053c40d65bafb9f6eed/botocore-1.42.37-py3-none-any.whl", hash = "sha256:f13bb8b560a10714d96fb7b0c7f17828dfa6e6606a1ead8c01c6ebb8765acbd8", size = 14589390, upload-time = "2026-01-28T20:38:31.306Z" },
-]
-
-[[package]]
-name = "cachetools"
-version = "6.2.6"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/39/91/d9ae9a66b01102a18cd16db0cf4cd54187ffe10f0865cc80071a4104fbb3/cachetools-6.2.6.tar.gz", hash = "sha256:16c33e1f276b9a9c0b49ab5782d901e3ad3de0dd6da9bf9bcd29ac5672f2f9e6", size = 32363, upload-time = "2026-01-27T20:32:59.956Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/90/45/f458fa2c388e79dd9d8b9b0c99f1d31b568f27388f2fdba7bb66bbc0c6ed/cachetools-6.2.6-py3-none-any.whl", hash = "sha256:8c9717235b3c651603fff0076db52d6acbfd1b338b8ed50256092f7ce9c85bda", size = 11668, upload-time = "2026-01-27T20:32:58.527Z" },
-]
-
-[[package]]
-name = "cattrs"
-version = "25.3.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "attrs" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/6e/00/2432bb2d445b39b5407f0a90e01b9a271475eea7caf913d7a86bcb956385/cattrs-25.3.0.tar.gz", hash = "sha256:1ac88d9e5eda10436c4517e390a4142d88638fe682c436c93db7ce4a277b884a", size = 509321, upload-time = "2025-10-07T12:26:08.737Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d8/2b/a40e1488fdfa02d3f9a653a61a5935ea08b3c2225ee818db6a76c7ba9695/cattrs-25.3.0-py3-none-any.whl", hash = "sha256:9896e84e0a5bf723bc7b4b68f4481785367ce07a8a02e7e9ee6eb2819bc306ff", size = 70738, upload-time = "2025-10-07T12:26:06.603Z" },
-]
-
-[[package]]
-name = "certifi"
-version = "2026.1.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
-]
-
-[[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" },
-]
-
-[[package]]
-name = "charset-normalizer"
-version = "3.4.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
- { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
- { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
- { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" },
- { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" },
- { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" },
- { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" },
- { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" },
- { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" },
- { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" },
- { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" },
- { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" },
- { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" },
- { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
- { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
- { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
- { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
- { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
- { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
- { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
- { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
- { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
- { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
- { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
- { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
- { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
- { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
- { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
- { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
- { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
- { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
- { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
- { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
-]
-
-[[package]]
-name = "click"
-version = "8.3.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
-]
-
-[[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 = "cryptography"
-version = "46.0.4"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/78/19/f748958276519adf6a0c1e79e7b8860b4830dda55ccdf29f2719b5fc499c/cryptography-46.0.4.tar.gz", hash = "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59", size = 749301, upload-time = "2026-01-28T00:24:37.379Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/8d/99/157aae7949a5f30d51fcb1a9851e8ebd5c74bf99b5285d8bb4b8b9ee641e/cryptography-46.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485", size = 7173686, upload-time = "2026-01-28T00:23:07.515Z" },
- { url = "https://files.pythonhosted.org/packages/87/91/874b8910903159043b5c6a123b7e79c4559ddd1896e38967567942635778/cryptography-46.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc", size = 4275871, upload-time = "2026-01-28T00:23:09.439Z" },
- { url = "https://files.pythonhosted.org/packages/c0/35/690e809be77896111f5b195ede56e4b4ed0435b428c2f2b6d35046fbb5e8/cryptography-46.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0", size = 4423124, upload-time = "2026-01-28T00:23:11.529Z" },
- { url = "https://files.pythonhosted.org/packages/1a/5b/a26407d4f79d61ca4bebaa9213feafdd8806dc69d3d290ce24996d3cfe43/cryptography-46.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa", size = 4277090, upload-time = "2026-01-28T00:23:13.123Z" },
- { url = "https://files.pythonhosted.org/packages/0c/d8/4bb7aec442a9049827aa34cee1aa83803e528fa55da9a9d45d01d1bb933e/cryptography-46.0.4-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5aa3e463596b0087b3da0dbe2b2487e9fc261d25da85754e30e3b40637d61f81", size = 4947652, upload-time = "2026-01-28T00:23:14.554Z" },
- { url = "https://files.pythonhosted.org/packages/2b/08/f83e2e0814248b844265802d081f2fac2f1cbe6cd258e72ba14ff006823a/cryptography-46.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255", size = 4455157, upload-time = "2026-01-28T00:23:16.443Z" },
- { url = "https://files.pythonhosted.org/packages/0a/05/19d849cf4096448779d2dcc9bb27d097457dac36f7273ffa875a93b5884c/cryptography-46.0.4-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e", size = 3981078, upload-time = "2026-01-28T00:23:17.838Z" },
- { url = "https://files.pythonhosted.org/packages/e6/89/f7bac81d66ba7cde867a743ea5b37537b32b5c633c473002b26a226f703f/cryptography-46.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c", size = 4276213, upload-time = "2026-01-28T00:23:19.257Z" },
- { url = "https://files.pythonhosted.org/packages/da/9f/7133e41f24edd827020ad21b068736e792bc68eecf66d93c924ad4719fb3/cryptography-46.0.4-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:eeeb2e33d8dbcccc34d64651f00a98cb41b2dc69cef866771a5717e6734dfa32", size = 4912190, upload-time = "2026-01-28T00:23:21.244Z" },
- { url = "https://files.pythonhosted.org/packages/a6/f7/6d43cbaddf6f65b24816e4af187d211f0bc536a29961f69faedc48501d8e/cryptography-46.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616", size = 4454641, upload-time = "2026-01-28T00:23:22.866Z" },
- { url = "https://files.pythonhosted.org/packages/9e/4f/ebd0473ad656a0ac912a16bd07db0f5d85184924e14fc88feecae2492834/cryptography-46.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0", size = 4405159, upload-time = "2026-01-28T00:23:25.278Z" },
- { url = "https://files.pythonhosted.org/packages/d1/f7/7923886f32dc47e27adeff8246e976d77258fd2aa3efdd1754e4e323bf49/cryptography-46.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0", size = 4666059, upload-time = "2026-01-28T00:23:26.766Z" },
- { url = "https://files.pythonhosted.org/packages/eb/a7/0fca0fd3591dffc297278a61813d7f661a14243dd60f499a7a5b48acb52a/cryptography-46.0.4-cp311-abi3-win32.whl", hash = "sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5", size = 3026378, upload-time = "2026-01-28T00:23:28.317Z" },
- { url = "https://files.pythonhosted.org/packages/2d/12/652c84b6f9873f0909374864a57b003686c642ea48c84d6c7e2c515e6da5/cryptography-46.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b", size = 3478614, upload-time = "2026-01-28T00:23:30.275Z" },
- { url = "https://files.pythonhosted.org/packages/56/f7/f648fdbb61d0d45902d3f374217451385edc7e7768d1b03ff1d0e5ffc17b/cryptography-46.0.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab", size = 7169583, upload-time = "2026-01-28T00:23:56.558Z" },
- { url = "https://files.pythonhosted.org/packages/d8/cc/8f3224cbb2a928de7298d6ed4790f5ebc48114e02bdc9559196bfb12435d/cryptography-46.0.4-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef", size = 4275419, upload-time = "2026-01-28T00:23:58.364Z" },
- { url = "https://files.pythonhosted.org/packages/17/43/4a18faa7a872d00e4264855134ba82d23546c850a70ff209e04ee200e76f/cryptography-46.0.4-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d", size = 4419058, upload-time = "2026-01-28T00:23:59.867Z" },
- { url = "https://files.pythonhosted.org/packages/ee/64/6651969409821d791ba12346a124f55e1b76f66a819254ae840a965d4b9c/cryptography-46.0.4-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973", size = 4278151, upload-time = "2026-01-28T00:24:01.731Z" },
- { url = "https://files.pythonhosted.org/packages/20/0b/a7fce65ee08c3c02f7a8310cc090a732344066b990ac63a9dfd0a655d321/cryptography-46.0.4-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a90e43e3ef65e6dcf969dfe3bb40cbf5aef0d523dff95bfa24256be172a845f4", size = 4939441, upload-time = "2026-01-28T00:24:03.175Z" },
- { url = "https://files.pythonhosted.org/packages/db/a7/20c5701e2cd3e1dfd7a19d2290c522a5f435dd30957d431dcb531d0f1413/cryptography-46.0.4-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af", size = 4451617, upload-time = "2026-01-28T00:24:05.403Z" },
- { url = "https://files.pythonhosted.org/packages/00/dc/3e16030ea9aa47b63af6524c354933b4fb0e352257c792c4deeb0edae367/cryptography-46.0.4-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263", size = 3977774, upload-time = "2026-01-28T00:24:06.851Z" },
- { url = "https://files.pythonhosted.org/packages/42/c8/ad93f14118252717b465880368721c963975ac4b941b7ef88f3c56bf2897/cryptography-46.0.4-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095", size = 4277008, upload-time = "2026-01-28T00:24:08.926Z" },
- { url = "https://files.pythonhosted.org/packages/00/cf/89c99698151c00a4631fbfcfcf459d308213ac29e321b0ff44ceeeac82f1/cryptography-46.0.4-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d5a45ddc256f492ce42a4e35879c5e5528c09cd9ad12420828c972951d8e016b", size = 4903339, upload-time = "2026-01-28T00:24:12.009Z" },
- { url = "https://files.pythonhosted.org/packages/03/c3/c90a2cb358de4ac9309b26acf49b2a100957e1ff5cc1e98e6c4996576710/cryptography-46.0.4-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019", size = 4451216, upload-time = "2026-01-28T00:24:13.975Z" },
- { url = "https://files.pythonhosted.org/packages/96/2c/8d7f4171388a10208671e181ca43cdc0e596d8259ebacbbcfbd16de593da/cryptography-46.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4", size = 4404299, upload-time = "2026-01-28T00:24:16.169Z" },
- { url = "https://files.pythonhosted.org/packages/e9/23/cbb2036e450980f65c6e0a173b73a56ff3bccd8998965dea5cc9ddd424a5/cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b", size = 4664837, upload-time = "2026-01-28T00:24:17.629Z" },
- { url = "https://files.pythonhosted.org/packages/0a/21/f7433d18fe6d5845329cbdc597e30caf983229c7a245bcf54afecc555938/cryptography-46.0.4-cp38-abi3-win32.whl", hash = "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc", size = 3009779, upload-time = "2026-01-28T00:24:20.198Z" },
- { url = "https://files.pythonhosted.org/packages/3a/6a/bd2e7caa2facffedf172a45c1a02e551e6d7d4828658c9a245516a598d94/cryptography-46.0.4-cp38-abi3-win_amd64.whl", hash = "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976", size = 3466633, upload-time = "2026-01-28T00:24:21.851Z" },
- { url = "https://files.pythonhosted.org/packages/59/e0/f9c6c53e1f2a1c2507f00f2faba00f01d2f334b35b0fbfe5286715da2184/cryptography-46.0.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:766330cce7416c92b5e90c3bb71b1b79521760cdcfc3a6a1a182d4c9fab23d2b", size = 3476316, upload-time = "2026-01-28T00:24:24.144Z" },
- { url = "https://files.pythonhosted.org/packages/27/7a/f8d2d13227a9a1a9fe9c7442b057efecffa41f1e3c51d8622f26b9edbe8f/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c236a44acfb610e70f6b3e1c3ca20ff24459659231ef2f8c48e879e2d32b73da", size = 4216693, upload-time = "2026-01-28T00:24:25.758Z" },
- { url = "https://files.pythonhosted.org/packages/c5/de/3787054e8f7972658370198753835d9d680f6cd4a39df9f877b57f0dd69c/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8a15fb869670efa8f83cbffbc8753c1abf236883225aed74cd179b720ac9ec80", size = 4382765, upload-time = "2026-01-28T00:24:27.577Z" },
- { url = "https://files.pythonhosted.org/packages/8a/5f/60e0afb019973ba6a0b322e86b3d61edf487a4f5597618a430a2a15f2d22/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:fdc3daab53b212472f1524d070735b2f0c214239df131903bae1d598016fa822", size = 4216066, upload-time = "2026-01-28T00:24:29.056Z" },
- { url = "https://files.pythonhosted.org/packages/81/8e/bf4a0de294f147fee66f879d9bae6f8e8d61515558e3d12785dd90eca0be/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:44cc0675b27cadb71bdbb96099cca1fa051cd11d2ade09e5cd3a2edb929ed947", size = 4382025, upload-time = "2026-01-28T00:24:30.681Z" },
- { url = "https://files.pythonhosted.org/packages/79/f4/9ceb90cfd6a3847069b0b0b353fd3075dc69b49defc70182d8af0c4ca390/cryptography-46.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be8c01a7d5a55f9a47d1888162b76c8f49d62b234d88f0ff91a9fbebe32ffbc3", size = 3406043, upload-time = "2026-01-28T00:24:32.236Z" },
-]
-
-[[package]]
-name = "dill"
-version = "0.4.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" },
-]
-
-[[package]]
-name = "diskcache"
-version = "5.6.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" },
-]
-
-[[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.17.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" },
-]
-
-[[package]]
-name = "eval-type-backport"
-version = "0.2.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/30/ea/8b0ac4469d4c347c6a385ff09dc3c048c2d021696664e26c7ee6791631b5/eval_type_backport-0.2.2.tar.gz", hash = "sha256:f0576b4cf01ebb5bd358d02314d31846af5e07678387486e2c798af0e7d849c1", size = 9079, upload-time = "2024-12-21T20:09:46.005Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ce/31/55cd413eaccd39125368be33c46de24a1f639f2e12349b0361b4678f3915/eval_type_backport-0.2.2-py3-none-any.whl", hash = "sha256:cb6ad7c393517f476f96d456d0412ea80f0a8cf96f6892834cd9340149111b0a", size = 5830, upload-time = "2024-12-21T20:09:44.175Z" },
-]
-
-[[package]]
-name = "executing"
-version = "2.2.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" },
-]
-
-[[package]]
-name = "filelock"
-version = "3.20.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" },
-]
-
-[[package]]
-name = "frozenlist"
-version = "1.8.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" },
- { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" },
- { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" },
- { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" },
- { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" },
- { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" },
- { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" },
- { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" },
- { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" },
- { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" },
- { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" },
- { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" },
- { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" },
- { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" },
- { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" },
- { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" },
- { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" },
- { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" },
- { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" },
- { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" },
- { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" },
- { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" },
- { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" },
- { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" },
- { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" },
- { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" },
- { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" },
- { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" },
- { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" },
- { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" },
- { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" },
- { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" },
- { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
-]
-
-[[package]]
-name = "google-auth"
-version = "2.48.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "cryptography" },
- { name = "pyasn1-modules" },
- { name = "rsa" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/0c/41/242044323fbd746615884b1c16639749e73665b718209946ebad7ba8a813/google_auth-2.48.0.tar.gz", hash = "sha256:4f7e706b0cd3208a3d940a19a822c37a476ddba5450156c3e6624a71f7c841ce", size = 326522, upload-time = "2026-01-26T19:22:47.157Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f", size = 236499, upload-time = "2026-01-26T19:22:45.099Z" },
-]
-
-[package.optional-dependencies]
-requests = [
- { name = "requests" },
-]
-
-[[package]]
-name = "google-genai"
-version = "1.60.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/0a/3f/a753be0dcee352b7d63bc6d1ba14a72591d63b6391dac0cdff7ac168c530/google_genai-1.60.0.tar.gz", hash = "sha256:9768061775fddfaecfefb0d6d7a6cabefb3952ebd246cd5f65247151c07d33d1", size = 487721, upload-time = "2026-01-21T22:17:30.398Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/31/e5/384b1f383917b5f0ae92e28f47bc27b16e3d26cd9bacb25e9f8ecab3c8fe/google_genai-1.60.0-py3-none-any.whl", hash = "sha256:967338378ffecebec19a8ed90cf8797b26818bacbefd7846a9280beb1099f7f3", size = 719431, upload-time = "2026-01-21T22:17:28.086Z" },
-]
-
-[[package]]
-name = "groq"
-version = "1.0.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "anyio" },
- { name = "distro" },
- { name = "httpx" },
- { name = "pydantic" },
- { name = "sniffio" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/3f/12/f4099a141677fcd2ed79dcc1fcec431e60c52e0e90c9c5d935f0ffaf8c0e/groq-1.0.0.tar.gz", hash = "sha256:66cb7bb729e6eb644daac7ce8efe945e99e4eb33657f733ee6f13059ef0c25a9", size = 146068, upload-time = "2025-12-17T23:34:23.115Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/4a/88/3175759d2ef30406ea721f4d837bfa1ba4339fde3b81ba8c5640a96ed231/groq-1.0.0-py3-none-any.whl", hash = "sha256:6e22bf92ffad988f01d2d4df7729add66b8fd5dbfb2154b5bbf3af245b72c731", size = 138292, upload-time = "2025-12-17T23:34:21.957Z" },
-]
-
-[[package]]
-name = "grpclib"
-version = "0.4.9"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "h2" },
- { name = "multidict" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/5b/28/5a2c299ec82a876a252c5919aa895a6f1d1d35c96417c5ce4a4660dc3a80/grpclib-0.4.9.tar.gz", hash = "sha256:cc589c330fa81004c6400a52a566407574498cb5b055fa927013361e21466c46", size = 84798, upload-time = "2025-12-14T22:23:14.349Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/5c/90/b0cbbd9efcc82816c58f31a34963071aa19fb792a212a5d9caf8e0fc3097/grpclib-0.4.9-py3-none-any.whl", hash = "sha256:7762ec1c8ed94dfad597475152dd35cbd11aecaaca2f243e29702435ca24cf0e", size = 77063, upload-time = "2025-12-14T22:23:13.224Z" },
-]
-
-[[package]]
-name = "h11"
-version = "0.16.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
-]
-
-[[package]]
-name = "h2"
-version = "4.3.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "hpack" },
- { name = "hyperframe" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" },
-]
-
-[[package]]
-name = "hpack"
-version = "4.1.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" },
-]
-
-[[package]]
-name = "httpcore"
-version = "1.0.9"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "certifi" },
- { name = "h11" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
-]
-
-[[package]]
-name = "httpx"
-version = "0.28.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "anyio" },
- { name = "certifi" },
- { name = "httpcore" },
- { name = "idna" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
-]
-
-[[package]]
-name = "hyperframe"
-version = "6.1.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" },
-]
-
-[[package]]
-name = "idna"
-version = "3.11"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
-]
-
-[[package]]
-name = "imbue-core"
-version = "0.0.9"
-source = { editable = "." }
-dependencies = [
- { name = "anthropic" },
- { name = "anyio" },
- { name = "attrs" },
- { name = "boto3" },
- { name = "cachetools" },
- { name = "cattrs" },
- { name = "diskcache" },
- { name = "google-genai" },
- { name = "groq" },
- { name = "grpclib" },
- { name = "httpx" },
- { name = "inline-snapshot" },
- { name = "loguru" },
- { name = "openai" },
- { name = "pathspec" },
- { name = "posthog" },
- { name = "prometheus-client" },
- { name = "pydantic" },
- { name = "pydantic-settings" },
- { name = "pygit2" },
- { name = "pygments" },
- { name = "pyhumps" },
- { name = "pylint" },
- { name = "pytest" },
- { name = "pytest-asyncio" },
- { name = "pytest-mock" },
- { name = "python-gitlab" },
- { name = "sentry-sdk" },
- { name = "syrupy" },
- { name = "tblib" },
- { name = "tenacity" },
- { name = "tiktoken" },
- { name = "together" },
- { name = "toml" },
- { name = "traceback-with-variables" },
- { name = "typeid-python" },
- { name = "yasoo" },
-]
-
-[package.dev-dependencies]
-dev = [
- { name = "moto" },
-]
-
-[package.metadata]
-requires-dist = [
- { name = "anthropic", specifier = "~=0.54" },
- { name = "anyio" },
- { name = "attrs" },
- { name = "boto3", specifier = ">=1.38.27" },
- { name = "cachetools" },
- { name = "cattrs" },
- { name = "diskcache", specifier = ">=5.6.3" },
- { name = "google-genai", specifier = ">=1.26.0" },
- { name = "groq", specifier = ">=0.18.0" },
- { name = "grpclib", specifier = ">=0.4.7" },
- { name = "httpx" },
- { name = "inline-snapshot" },
- { name = "loguru" },
- { name = "openai", specifier = ">=1.79.0" },
- { name = "pathspec" },
- { name = "posthog", specifier = "==5.4.0" },
- { name = "prometheus-client", specifier = ">=0.20.0" },
- { name = "pydantic", specifier = ">=2.11.4" },
- { name = "pydantic-settings" },
- { name = "pygit2", specifier = ">=1.18.0" },
- { name = "pygments", specifier = ">=2.0.0" },
- { name = "pyhumps" },
- { name = "pylint", specifier = "==3.2.6" },
- { name = "pytest" },
- { name = "pytest-asyncio" },
- { name = "pytest-mock" },
- { name = "python-gitlab", specifier = ">=4.5.0" },
- { name = "sentry-sdk" },
- { name = "syrupy" },
- { name = "tblib", specifier = "==2.0.0" },
- { name = "tenacity", specifier = ">=8.2.2" },
- { name = "tiktoken" },
- { name = "together" },
- { name = "toml" },
- { name = "traceback-with-variables", specifier = ">=2.2.0" },
- { name = "typeid-python" },
- { name = "yasoo" },
-]
-
-[package.metadata.requires-dev]
-dev = [{ name = "moto", specifier = ">=4.1.12" }]
-
-[[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 = "inline-snapshot"
-version = "0.31.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "asttokens" },
- { name = "executing" },
- { name = "pytest" },
- { name = "rich" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/1c/b1/52b5ee59f73ed31d5fe21b10881bf2d121d07d54b23c0b6b74186792e620/inline_snapshot-0.31.1.tar.gz", hash = "sha256:4ea5ed70aa1d652713bbfd750606b94bd8a42483f7d3680433b3e92994495f64", size = 2606338, upload-time = "2025-11-07T07:36:18.932Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ba/52/945db420380efbda8c69a7a4a16c53df9d7ac50d8217286b9d41e5d825ff/inline_snapshot-0.31.1-py3-none-any.whl", hash = "sha256:7875a73c986a03388c7e758fb5cb8a43d2c3a20328aa1d851bfb4ed536c4496f", size = 71965, upload-time = "2025-11-07T07:36:16.836Z" },
-]
-
-[[package]]
-name = "isort"
-version = "5.13.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/87/f9/c1eb8635a24e87ade2efce21e3ce8cd6b8630bb685ddc9cdaca1349b2eb5/isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", size = 175303, upload-time = "2023-12-13T20:37:26.124Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d1/b3/8def84f539e7d2289a02f0524b944b15d7c75dab7628bedf1c4f0992029c/isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6", size = 92310, upload-time = "2023-12-13T20:37:23.244Z" },
-]
-
-[[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.12.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435, upload-time = "2025-11-09T20:47:02.087Z" },
- { url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548, upload-time = "2025-11-09T20:47:03.409Z" },
- { url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915, upload-time = "2025-11-09T20:47:05.171Z" },
- { url = "https://files.pythonhosted.org/packages/15/2d/c06e659888c128ad1e838123d0638f0efad90cc30860cb5f74dd3f2fc0b3/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966, upload-time = "2025-11-09T20:47:06.508Z" },
- { url = "https://files.pythonhosted.org/packages/6b/20/058db4ae5fb07cf6a4ab2e9b9294416f606d8e467fb74c2184b2a1eeacba/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047, upload-time = "2025-11-09T20:47:08.382Z" },
- { url = "https://files.pythonhosted.org/packages/49/bb/dc2b1c122275e1de2eb12905015d61e8316b2f888bdaac34221c301495d6/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835, upload-time = "2025-11-09T20:47:09.81Z" },
- { url = "https://files.pythonhosted.org/packages/23/7d/38f9cd337575349de16da575ee57ddb2d5a64d425c9367f5ef9e4612e32e/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587, upload-time = "2025-11-09T20:47:11.529Z" },
- { url = "https://files.pythonhosted.org/packages/f0/a3/b13e8e61e70f0bb06085099c4e2462647f53cc2ca97614f7fedcaa2bb9f3/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492, upload-time = "2025-11-09T20:47:12.993Z" },
- { url = "https://files.pythonhosted.org/packages/07/71/e0d11422ed027e21422f7bc1883c61deba2d9752b720538430c1deadfbca/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046, upload-time = "2025-11-09T20:47:14.6Z" },
- { url = "https://files.pythonhosted.org/packages/9f/59/b968a9aa7102a8375dbbdfbd2aeebe563c7e5dddf0f47c9ef1588a97e224/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392, upload-time = "2025-11-09T20:47:16.011Z" },
- { url = "https://files.pythonhosted.org/packages/ca/e4/7df62002499080dbd61b505c5cb351aa09e9959d176cac2aa8da6f93b13b/jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c", size = 206096, upload-time = "2025-11-09T20:47:17.344Z" },
- { url = "https://files.pythonhosted.org/packages/bb/60/1032b30ae0572196b0de0e87dce3b6c26a1eff71aad5fe43dee3082d32e0/jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f", size = 204899, upload-time = "2025-11-09T20:47:19.365Z" },
- { url = "https://files.pythonhosted.org/packages/49/d5/c145e526fccdb834063fb45c071df78b0cc426bbaf6de38b0781f45d956f/jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5", size = 188070, upload-time = "2025-11-09T20:47:20.75Z" },
- { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" },
- { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" },
- { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" },
- { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" },
- { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" },
- { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" },
- { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" },
- { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" },
- { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" },
- { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" },
- { url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546, upload-time = "2025-11-09T20:47:40.47Z" },
- { url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196, upload-time = "2025-11-09T20:47:41.794Z" },
- { url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100, upload-time = "2025-11-09T20:47:43.007Z" },
- { url = "https://files.pythonhosted.org/packages/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144, upload-time = "2025-11-09T20:49:10.503Z" },
- { url = "https://files.pythonhosted.org/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877, upload-time = "2025-11-09T20:49:12.269Z" },
- { url = "https://files.pythonhosted.org/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419, upload-time = "2025-11-09T20:49:13.803Z" },
- { url = "https://files.pythonhosted.org/packages/21/01/857d4608f5edb0664aa791a3d45702e1a5bcfff9934da74035e7b9803846/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d", size = 347212, upload-time = "2025-11-09T20:49:15.643Z" },
- { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" },
- { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" },
- { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" },
- { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" },
-]
-
-[[package]]
-name = "jmespath"
-version = "1.1.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" },
-]
-
-[[package]]
-name = "loguru"
-version = "0.7.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
- { name = "win32-setctime", marker = "sys_platform == 'win32'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" },
-]
-
-[[package]]
-name = "markdown-it-py"
-version = "4.0.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "mdurl" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
-]
-
-[[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" },
-]
-
-[[package]]
-name = "mccabe"
-version = "0.7.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" },
-]
-
-[[package]]
-name = "mdurl"
-version = "0.1.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
-]
-
-[[package]]
-name = "more-itertools"
-version = "10.8.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" },
-]
-
-[[package]]
-name = "moto"
-version = "5.1.20"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "boto3" },
- { name = "botocore" },
- { name = "cryptography" },
- { name = "jinja2" },
- { name = "python-dateutil" },
- { name = "requests" },
- { name = "responses" },
- { name = "werkzeug" },
- { name = "xmltodict" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/b4/93/6b696aab5174721696a17716a488086e21f7b2547b4c9517f799a9b25e9e/moto-5.1.20.tar.gz", hash = "sha256:6d12d781e26a550d80e4b7e01d5538178e3adec6efbdec870e06e84750f13ec0", size = 8318716, upload-time = "2026-01-17T21:49:00.101Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7f/2f/f50892fdb28097917b87d358a5fcefd30976289884ff142893edcb0243ba/moto-5.1.20-py3-none-any.whl", hash = "sha256:58c82c8e6b2ef659ef3a562fa415dce14da84bc7a797943245d9a338496ea0ea", size = 6392751, upload-time = "2026-01-17T21:48:57.099Z" },
-]
-
-[[package]]
-name = "multidict"
-version = "6.7.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" },
- { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" },
- { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" },
- { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" },
- { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" },
- { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" },
- { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" },
- { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" },
- { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" },
- { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" },
- { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" },
- { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" },
- { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" },
- { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" },
- { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" },
- { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" },
- { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" },
- { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" },
- { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" },
- { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" },
- { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" },
- { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" },
- { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" },
- { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" },
- { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" },
- { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" },
- { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" },
- { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" },
- { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" },
- { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" },
- { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" },
- { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" },
- { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" },
- { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" },
- { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" },
- { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" },
- { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" },
-]
-
-[[package]]
-name = "mypy-extensions"
-version = "1.1.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
-]
-
-[[package]]
-name = "numpy"
-version = "2.4.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/24/62/ae72ff66c0f1fd959925b4c11f8c2dea61f47f6acaea75a08512cdfe3fed/numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690", size = 20721320, upload-time = "2026-01-10T06:44:59.619Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a5/34/2b1bc18424f3ad9af577f6ce23600319968a70575bd7db31ce66731bbef9/numpy-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0cce2a669e3c8ba02ee563c7835f92c153cf02edff1ae05e1823f1dde21b16a5", size = 16944563, upload-time = "2026-01-10T06:42:14.615Z" },
- { url = "https://files.pythonhosted.org/packages/2c/57/26e5f97d075aef3794045a6ca9eada6a4ed70eb9a40e7a4a93f9ac80d704/numpy-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:899d2c18024984814ac7e83f8f49d8e8180e2fbe1b2e252f2e7f1d06bea92425", size = 12645658, upload-time = "2026-01-10T06:42:17.298Z" },
- { url = "https://files.pythonhosted.org/packages/8e/ba/80fc0b1e3cb2fd5c6143f00f42eb67762aa043eaa05ca924ecc3222a7849/numpy-2.4.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:09aa8a87e45b55a1c2c205d42e2808849ece5c484b2aab11fecabec3841cafba", size = 5474132, upload-time = "2026-01-10T06:42:19.637Z" },
- { url = "https://files.pythonhosted.org/packages/40/ae/0a5b9a397f0e865ec171187c78d9b57e5588afc439a04ba9cab1ebb2c945/numpy-2.4.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:edee228f76ee2dab4579fad6f51f6a305de09d444280109e0f75df247ff21501", size = 6804159, upload-time = "2026-01-10T06:42:21.44Z" },
- { url = "https://files.pythonhosted.org/packages/86/9c/841c15e691c7085caa6fd162f063eff494099c8327aeccd509d1ab1e36ab/numpy-2.4.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a92f227dbcdc9e4c3e193add1a189a9909947d4f8504c576f4a732fd0b54240a", size = 14708058, upload-time = "2026-01-10T06:42:23.546Z" },
- { url = "https://files.pythonhosted.org/packages/5d/9d/7862db06743f489e6a502a3b93136d73aea27d97b2cf91504f70a27501d6/numpy-2.4.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:538bf4ec353709c765ff75ae616c34d3c3dca1a68312727e8f2676ea644f8509", size = 16651501, upload-time = "2026-01-10T06:42:25.909Z" },
- { url = "https://files.pythonhosted.org/packages/a6/9c/6fc34ebcbd4015c6e5f0c0ce38264010ce8a546cb6beacb457b84a75dfc8/numpy-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ac08c63cb7779b85e9d5318e6c3518b424bc1f364ac4cb2c6136f12e5ff2dccc", size = 16492627, upload-time = "2026-01-10T06:42:28.938Z" },
- { url = "https://files.pythonhosted.org/packages/aa/63/2494a8597502dacda439f61b3c0db4da59928150e62be0e99395c3ad23c5/numpy-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f9c360ecef085e5841c539a9a12b883dff005fbd7ce46722f5e9cef52634d82", size = 18585052, upload-time = "2026-01-10T06:42:31.312Z" },
- { url = "https://files.pythonhosted.org/packages/6a/93/098e1162ae7522fc9b618d6272b77404c4656c72432ecee3abc029aa3de0/numpy-2.4.1-cp311-cp311-win32.whl", hash = "sha256:0f118ce6b972080ba0758c6087c3617b5ba243d806268623dc34216d69099ba0", size = 6236575, upload-time = "2026-01-10T06:42:33.872Z" },
- { url = "https://files.pythonhosted.org/packages/8c/de/f5e79650d23d9e12f38a7bc6b03ea0835b9575494f8ec94c11c6e773b1b1/numpy-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:18e14c4d09d55eef39a6ab5b08406e84bc6869c1e34eef45564804f90b7e0574", size = 12604479, upload-time = "2026-01-10T06:42:35.778Z" },
- { url = "https://files.pythonhosted.org/packages/dd/65/e1097a7047cff12ce3369bd003811516b20ba1078dbdec135e1cd7c16c56/numpy-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:6461de5113088b399d655d45c3897fa188766415d0f568f175ab071c8873bd73", size = 10578325, upload-time = "2026-01-10T06:42:38.518Z" },
- { url = "https://files.pythonhosted.org/packages/78/7f/ec53e32bf10c813604edf07a3682616bd931d026fcde7b6d13195dfb684a/numpy-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2", size = 16656888, upload-time = "2026-01-10T06:42:40.913Z" },
- { url = "https://files.pythonhosted.org/packages/b8/e0/1f9585d7dae8f14864e948fd7fa86c6cb72dee2676ca2748e63b1c5acfe0/numpy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8", size = 12373956, upload-time = "2026-01-10T06:42:43.091Z" },
- { url = "https://files.pythonhosted.org/packages/8e/43/9762e88909ff2326f5e7536fa8cb3c49fb03a7d92705f23e6e7f553d9cb3/numpy-2.4.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a", size = 5202567, upload-time = "2026-01-10T06:42:45.107Z" },
- { url = "https://files.pythonhosted.org/packages/4b/ee/34b7930eb61e79feb4478800a4b95b46566969d837546aa7c034c742ef98/numpy-2.4.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0", size = 6549459, upload-time = "2026-01-10T06:42:48.152Z" },
- { url = "https://files.pythonhosted.org/packages/79/e3/5f115fae982565771be994867c89bcd8d7208dbfe9469185497d70de5ddf/numpy-2.4.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c", size = 14404859, upload-time = "2026-01-10T06:42:49.947Z" },
- { url = "https://files.pythonhosted.org/packages/d9/7d/9c8a781c88933725445a859cac5d01b5871588a15969ee6aeb618ba99eee/numpy-2.4.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02", size = 16371419, upload-time = "2026-01-10T06:42:52.409Z" },
- { url = "https://files.pythonhosted.org/packages/a6/d2/8aa084818554543f17cf4162c42f162acbd3bb42688aefdba6628a859f77/numpy-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162", size = 16182131, upload-time = "2026-01-10T06:42:54.694Z" },
- { url = "https://files.pythonhosted.org/packages/60/db/0425216684297c58a8df35f3284ef56ec4a043e6d283f8a59c53562caf1b/numpy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9", size = 18295342, upload-time = "2026-01-10T06:42:56.991Z" },
- { url = "https://files.pythonhosted.org/packages/31/4c/14cb9d86240bd8c386c881bafbe43f001284b7cce3bc01623ac9475da163/numpy-2.4.1-cp312-cp312-win32.whl", hash = "sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f", size = 5959015, upload-time = "2026-01-10T06:42:59.631Z" },
- { url = "https://files.pythonhosted.org/packages/51/cf/52a703dbeb0c65807540d29699fef5fda073434ff61846a564d5c296420f/numpy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87", size = 12310730, upload-time = "2026-01-10T06:43:01.627Z" },
- { url = "https://files.pythonhosted.org/packages/69/80/a828b2d0ade5e74a9fe0f4e0a17c30fdc26232ad2bc8c9f8b3197cf7cf18/numpy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8", size = 10312166, upload-time = "2026-01-10T06:43:03.673Z" },
- { url = "https://files.pythonhosted.org/packages/1e/48/d86f97919e79314a1cdee4c832178763e6e98e623e123d0bada19e92c15a/numpy-2.4.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ad35f20be147a204e28b6a0575fbf3540c5e5f802634d4258d55b1ff5facce1", size = 16822202, upload-time = "2026-01-10T06:44:43.738Z" },
- { url = "https://files.pythonhosted.org/packages/51/e9/1e62a7f77e0f37dcfb0ad6a9744e65df00242b6ea37dfafb55debcbf5b55/numpy-2.4.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8097529164c0f3e32bb89412a0905d9100bf434d9692d9fc275e18dcf53c9344", size = 12569985, upload-time = "2026-01-10T06:44:45.945Z" },
- { url = "https://files.pythonhosted.org/packages/c7/7e/914d54f0c801342306fdcdce3e994a56476f1b818c46c47fc21ae968088c/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:ea66d2b41ca4a1630aae5507ee0a71647d3124d1741980138aa8f28f44dac36e", size = 5398484, upload-time = "2026-01-10T06:44:48.012Z" },
- { url = "https://files.pythonhosted.org/packages/1c/d8/9570b68584e293a33474e7b5a77ca404f1dcc655e40050a600dee81d27fb/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d3f8f0df9f4b8be57b3bf74a1d087fec68f927a2fab68231fdb442bf2c12e426", size = 6713216, upload-time = "2026-01-10T06:44:49.725Z" },
- { url = "https://files.pythonhosted.org/packages/33/9b/9dd6e2db8d49eb24f86acaaa5258e5f4c8ed38209a4ee9de2d1a0ca25045/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2023ef86243690c2791fd6353e5b4848eedaa88ca8a2d129f462049f6d484696", size = 14538937, upload-time = "2026-01-10T06:44:51.498Z" },
- { url = "https://files.pythonhosted.org/packages/53/87/d5bd995b0f798a37105b876350d346eea5838bd8f77ea3d7a48392f3812b/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8361ea4220d763e54cff2fbe7d8c93526b744f7cd9ddab47afeff7e14e8503be", size = 16479830, upload-time = "2026-01-10T06:44:53.931Z" },
- { url = "https://files.pythonhosted.org/packages/5b/c7/b801bf98514b6ae6475e941ac05c58e6411dd863ea92916bfd6d510b08c1/numpy-2.4.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4f1b68ff47680c2925f8063402a693ede215f0257f02596b1318ecdfb1d79e33", size = 12492579, upload-time = "2026-01-10T06:44:57.094Z" },
-]
-
-[[package]]
-name = "openai"
-version = "2.16.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/b1/6c/e4c964fcf1d527fdf4739e7cc940c60075a4114d50d03871d5d5b1e13a88/openai-2.16.0.tar.gz", hash = "sha256:42eaa22ca0d8ded4367a77374104d7a2feafee5bd60a107c3c11b5243a11cd12", size = 629649, upload-time = "2026-01-27T23:28:02.579Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/16/83/0315bf2cfd75a2ce8a7e54188e9456c60cec6c0cf66728ed07bd9859ff26/openai-2.16.0-py3-none-any.whl", hash = "sha256:5f46643a8f42899a84e80c38838135d7038e7718333ce61396994f887b09a59b", size = 1068612, upload-time = "2026-01-27T23:28:00.356Z" },
-]
-
-[[package]]
-name = "packaging"
-version = "26.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
-]
-
-[[package]]
-name = "pathspec"
-version = "1.0.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" },
-]
-
-[[package]]
-name = "pillow"
-version = "11.3.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" },
- { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" },
- { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" },
- { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" },
- { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" },
- { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" },
- { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" },
- { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" },
- { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" },
- { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" },
- { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" },
- { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" },
- { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" },
- { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" },
- { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" },
- { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" },
- { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" },
- { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" },
- { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" },
- { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" },
- { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" },
- { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" },
- { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" },
- { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" },
- { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" },
- { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" },
- { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" },
- { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" },
- { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" },
-]
-
-[[package]]
-name = "platformdirs"
-version = "4.5.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
-]
-
-[[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 = "posthog"
-version = "5.4.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "backoff" },
- { name = "distro" },
- { name = "python-dateutil" },
- { name = "requests" },
- { name = "six" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/48/20/60ae67bb9d82f00427946218d49e2e7e80fb41c15dc5019482289ec9ce8d/posthog-5.4.0.tar.gz", hash = "sha256:701669261b8d07cdde0276e5bc096b87f9e200e3b9589c5ebff14df658c5893c", size = 88076, upload-time = "2025-06-20T23:19:23.485Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/4f/98/e480cab9a08d1c09b1c59a93dade92c1bb7544826684ff2acbfd10fcfbd4/posthog-5.4.0-py3-none-any.whl", hash = "sha256:284dfa302f64353484420b52d4ad81ff5c2c2d1d607c4e2db602ac72761831bd", size = 105364, upload-time = "2025-06-20T23:19:22.001Z" },
-]
-
-[[package]]
-name = "prometheus-client"
-version = "0.24.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" },
-]
-
-[[package]]
-name = "propcache"
-version = "0.4.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" },
- { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" },
- { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" },
- { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" },
- { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" },
- { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" },
- { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" },
- { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" },
- { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" },
- { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" },
- { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" },
- { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" },
- { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" },
- { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" },
- { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" },
- { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" },
- { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" },
- { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" },
- { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" },
- { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" },
- { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" },
- { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" },
- { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" },
- { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" },
- { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" },
- { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" },
- { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" },
- { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" },
- { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" },
- { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" },
- { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" },
-]
-
-[[package]]
-name = "pyasn1"
-version = "0.6.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" },
-]
-
-[[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.12.5"
-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/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
-]
-
-[[package]]
-name = "pydantic-core"
-version = "2.41.5"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
- { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
- { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
- { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" },
- { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" },
- { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" },
- { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" },
- { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" },
- { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" },
- { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" },
- { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" },
- { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" },
- { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" },
- { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" },
- { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
- { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
- { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
- { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
- { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
- { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
- { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
- { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
- { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
- { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
- { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
- { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
- { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
- { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
- { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
- { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
- { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
- { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" },
- { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
- { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
- { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
- { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
- { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
- { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
- { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
- { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" },
- { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" },
- { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" },
- { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" },
- { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
-]
-
-[[package]]
-name = "pydantic-settings"
-version = "2.12.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pydantic" },
- { name = "python-dotenv" },
- { name = "typing-inspection" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" },
-]
-
-[[package]]
-name = "pygit2"
-version = "1.19.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "cffi" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/17/49/cf8350817de19f4cafe4ae47881e38f56d9bbebaa9e5ef31a5458af4bcf8/pygit2-1.19.1.tar.gz", hash = "sha256:3165f784aae56a309a27d8eeae7923d53da2e8f6094308c7f5b428deec925cf9", size = 800869, upload-time = "2025-12-29T11:47:48.618Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f8/4f/c8c29c4af2de6b8b7e086cad24e200ec7f165587caa77b7d2d495366204e/pygit2-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2b54f3a94648ac8e287f5e4333710d9fe05f9e09de3da232d50df753bb01b643", size = 5702353, upload-time = "2025-12-29T11:46:28.548Z" },
- { url = "https://files.pythonhosted.org/packages/b9/04/814b305804f067fd8d1cd7166dc3704900704a8fa71280703212abbacf9f/pygit2-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e46618a912fc984b8a9f4d8322704620f1315264359c7fa61c899128e23e226", size = 5691612, upload-time = "2025-12-29T11:46:30.754Z" },
- { url = "https://files.pythonhosted.org/packages/cb/04/61c84d1ab2585f50a2551199e4228f3a800635c482e451e93f2cd0c0ae3d/pygit2-1.19.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2eb386b3e98f7056d76bc7e805e8fce3cd0a773cbbb30b0f7e144c0ac37270f2", size = 6021372, upload-time = "2025-12-29T11:46:32.439Z" },
- { url = "https://files.pythonhosted.org/packages/be/7a/daca8780c72b0d5a56165e0bff3b76d2fa8e0a8f7269f40aa17f10ed0356/pygit2-1.19.1-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f41a9b866676922ac9b0ec60f0dc9735a5d1ba6bb34146a6212dc0012d7959f", size = 4623817, upload-time = "2025-12-29T11:46:33.964Z" },
- { url = "https://files.pythonhosted.org/packages/92/f6/d065bb189c9fd86c5e540eb264567b4fe3eb06447da1408c03a35e15096b/pygit2-1.19.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2cdc81ecffd990d8c6dce44a16b1dc4494b5dd5381d6e1f508e459c4bca09e0", size = 5781284, upload-time = "2025-12-29T11:46:35.703Z" },
- { url = "https://files.pythonhosted.org/packages/ad/8a/2b9195619a9a0dc6e25525e784f7474174614ebc064a91b2a2087952a583/pygit2-1.19.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a1c8645287556aa9b670886dbc0d5daa1d49040511940822fd43dbda13cfe4e8", size = 6027281, upload-time = "2025-12-29T11:46:37.331Z" },
- { url = "https://files.pythonhosted.org/packages/d7/b7/20837029e8f5177d4ac48396a4448d02dfe455e988bb722d43dc42f6b0af/pygit2-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e388d1eb0c44d92d8ff01b25eb9a969fc28748966843c2e26e9e084e42567f7d", size = 5750642, upload-time = "2025-12-29T11:46:38.626Z" },
- { url = "https://files.pythonhosted.org/packages/41/42/18cc94976a35451a5653abf047356f94b5f503b1c0b02223a6d9e72979d3/pygit2-1.19.1-cp311-cp311-win32.whl", hash = "sha256:815c0b12845253929f2275759d623b3b4093e67e6536d2463177e6ff1d9ff0df", size = 942173, upload-time = "2025-12-29T11:46:40.087Z" },
- { url = "https://files.pythonhosted.org/packages/61/19/590708fc3182d47b40f0274f80671ccdf9c1a8fa5a838b554e6fe15a2bb3/pygit2-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:93f4986b35984aaaa5e7613ceb1ba4c184d890589df60b0d8d74e7dccec1d8cb", size = 1159463, upload-time = "2025-12-29T11:46:41.338Z" },
- { url = "https://files.pythonhosted.org/packages/90/a8/a2c1eb6f8c5f30cb5633a3c21e60ee6be2e4a3148b302f578e4b48e769ef/pygit2-1.19.1-cp311-cp311-win_arm64.whl", hash = "sha256:fef27b206955e66e3a63664e2ec93821e00ce2d917f8b4eae87c738163c00e14", size = 966795, upload-time = "2025-12-29T11:46:42.842Z" },
- { url = "https://files.pythonhosted.org/packages/1a/36/0784870218794d6069bf8ebae55679964edc44b8e59279f4526aa1220569/pygit2-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8e6a4f4a711750c286a13cea0007b40f7466c4d741c3d9b223ffbc3bbfbafe7", size = 5700218, upload-time = "2025-12-29T11:46:44.537Z" },
- { url = "https://files.pythonhosted.org/packages/56/65/47206823900ddca606022025369ba3e136de9d2310585acac10d8cef81fd/pygit2-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3f2340a668eb3e2d8927dcbeb1a043d3a65d2dd39a913995b34fc437da5e73af", size = 5692231, upload-time = "2025-12-29T11:46:45.821Z" },
- { url = "https://files.pythonhosted.org/packages/19/27/c6b52f53ee16b9d7eaacc575f08add3c336f53b5561cf94fe41ceeab1589/pygit2-1.19.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe41f09b1e334c43def6636b1133d2f4c91a20d9a6691bb4e7558e42a31bcb4e", size = 6022217, upload-time = "2025-12-29T11:46:47.086Z" },
- { url = "https://files.pythonhosted.org/packages/dc/ac/41d7a1ed69e117e9cd99b2f40f63898f9725ac6c4245b2b531ae0b7e59da/pygit2-1.19.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:527e57133d30ff6ea96634da6bf428f7d551958207fa73f9e9a18582b885e192", size = 4622846, upload-time = "2025-12-29T11:46:48.679Z" },
- { url = "https://files.pythonhosted.org/packages/09/22/f8fc7860b7b7ba15f7bf802ae3bce52b3e765b48846db115cb1c8372f971/pygit2-1.19.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a9340cb85b7be40080186a9d4dbf712a6be8a842556acbbfb305baebfb854f3", size = 5785236, upload-time = "2025-12-29T11:46:50.24Z" },
- { url = "https://files.pythonhosted.org/packages/ec/62/ee9275c48ecc119a7f5c48209aaa06d5f71d8476703c7700182c49c8a7a8/pygit2-1.19.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:66ecfa69f2287f50ec95dfc04821219c2f664c4cd292c7b33c10ed9afe975132", size = 6028266, upload-time = "2025-12-29T11:46:51.5Z" },
- { url = "https://files.pythonhosted.org/packages/7c/98/311112a50e6e319921f06c20ff237360c10bb2e8a1f959361567e48835f3/pygit2-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:14c76ec968ae20a6689c7b6fa833ef546c7bc176127d71e7b67cb2345a9813fb", size = 5755041, upload-time = "2025-12-29T11:46:53.337Z" },
- { url = "https://files.pythonhosted.org/packages/e1/45/f6a24326fb94e56ddae9906e21d4e4a006a36131a3a73819be1177e30e93/pygit2-1.19.1-cp312-cp312-win32.whl", hash = "sha256:ffe94118d39f6969fda594224b2b6df1ae79306adaf090ede65bcaf1a41b3a81", size = 942948, upload-time = "2025-12-29T11:46:54.465Z" },
- { url = "https://files.pythonhosted.org/packages/a3/1a/912ee3a33ba665f82cf8ed0087e7446f1f8e117aba1627e0c4ccc9b2a8c5/pygit2-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:c2ee3f2e91b0a5674ab7cb373234c23cf5f1cf6d84e56e6d12ff3db21414cf47", size = 1159880, upload-time = "2025-12-29T11:46:55.523Z" },
- { url = "https://files.pythonhosted.org/packages/24/fc/784eeceab43c2b4978aa46f03c267409f2502331fa18d0a8e58116d143d0/pygit2-1.19.1-cp312-cp312-win_arm64.whl", hash = "sha256:c8747d968d8d6b9d390263907f014d38a0f67bd26d8243e5bc3384cb252ec3d3", size = 966904, upload-time = "2025-12-29T11:46:56.888Z" },
- { url = "https://files.pythonhosted.org/packages/45/01/607b8a400ffe46340df083d67cb05296f90e0d302d09addac5dc1afee47f/pygit2-1.19.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3c56ef9ac89e020ca005a39db4e045792b1ce98c2450a53f79815e9d831c006a", size = 5646594, upload-time = "2025-12-29T11:47:41.437Z" },
- { url = "https://files.pythonhosted.org/packages/18/59/45e517b86692120fd927b8949916203c50ffce0cd7a7124131d90d816fde/pygit2-1.19.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a6d89079f3af32f25abb8680eabea31143a4f02f3d1da6644c296ba89b6a2fc", size = 5644506, upload-time = "2025-12-29T11:47:42.779Z" },
- { url = "https://files.pythonhosted.org/packages/db/25/41c0c37c0f8b23677364d9f82ddbb1377d2342666045d39b508acc3d6f97/pygit2-1.19.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bfd44dc6f1d5b1165cc2097c39000c4a5cc05443d27a3a5f2791ad338f52b07", size = 5559864, upload-time = "2025-12-29T11:47:44.399Z" },
- { url = "https://files.pythonhosted.org/packages/76/c0/16ff6c4d732d8644ab84a5d48141b55f6b353e08da5ffcbee03a5c58c3a5/pygit2-1.19.1-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0aca00ff7e3420f9c06d9386b0bfc76c18fd8a2c5234412db0e200a6cc47ed03", size = 5312681, upload-time = "2025-12-29T11:47:46.022Z" },
- { url = "https://files.pythonhosted.org/packages/08/cc/f762a2378d148ae40766fcac3f1ae1b5d925ae80128422366788eea9f5e6/pygit2-1.19.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f89f047667a218b71ebc96c398aca1e5109f149045a8d59ca9fd4a557d1e932e", size = 1130023, upload-time = "2025-12-29T11:47:47.55Z" },
-]
-
-[[package]]
-name = "pygments"
-version = "2.19.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
-]
-
-[[package]]
-name = "pyhumps"
-version = "3.8.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/c4/83/fa6f8fb7accb21f39e8f2b6a18f76f6d90626bdb0a5e5448e5cc9b8ab014/pyhumps-3.8.0.tar.gz", hash = "sha256:498026258f7ee1a8e447c2e28526c0bea9407f9a59c03260aee4bd6c04d681a3", size = 9018, upload-time = "2022-10-21T10:38:59.496Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9e/11/a1938340ecb32d71e47ad4914843775011e6e9da59ba1229f181fef3119e/pyhumps-3.8.0-py3-none-any.whl", hash = "sha256:060e1954d9069f428232a1adda165db0b9d8dfdce1d265d36df7fbff540acfd6", size = 6095, upload-time = "2022-10-21T10:38:58.231Z" },
-]
-
-[[package]]
-name = "pylint"
-version = "3.2.6"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "astroid" },
- { name = "colorama", marker = "sys_platform == 'win32'" },
- { name = "dill" },
- { name = "isort" },
- { name = "mccabe" },
- { name = "platformdirs" },
- { name = "tomlkit" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/30/10/abee071c1d52b2bca48be40fe9f64ca878a77e0beef6504597e8c9c1ed84/pylint-3.2.6.tar.gz", hash = "sha256:a5d01678349454806cff6d886fb072294f56a58c4761278c97fb557d708e1eb3", size = 1510167, upload-time = "2024-07-21T19:48:38.032Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/09/88/1a406dd0b17a4796f025d8c937d8d56f97869cffa55c21d9edb07f5a3912/pylint-3.2.6-py3-none-any.whl", hash = "sha256:03c8e3baa1d9fb995b12c1dbe00aa6c4bcef210c2a2634374aedeb22fb4a8f8f", size = 519798, upload-time = "2024-07-21T19:48:34.788Z" },
-]
-
-[[package]]
-name = "pytest"
-version = "9.0.2"
-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/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
-]
-
-[[package]]
-name = "pytest-asyncio"
-version = "1.3.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pytest" },
- { name = "typing-extensions" },
-]
-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-mock"
-version = "3.15.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pytest" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" },
-]
-
-[[package]]
-name = "python-dateutil"
-version = "2.9.0.post0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "six" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
-]
-
-[[package]]
-name = "python-dotenv"
-version = "1.2.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
-]
-
-[[package]]
-name = "python-gitlab"
-version = "8.0.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "requests" },
- { name = "requests-toolbelt" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/c4/68/02645bc9d71554e7a263b118e4e55dafe4c4735c1ba74f9712232ed84054/python_gitlab-8.0.0.tar.gz", hash = "sha256:03eae5a9d105448796e6c0e192d402c266057e75790cf4f42c143dddf91313ce", size = 401334, upload-time = "2026-01-28T01:22:27.005Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/52/60/ba68e51e90a99b14af639463e5d617239029ec25927a0990ff28bd851916/python_gitlab-8.0.0-py3-none-any.whl", hash = "sha256:c635e6722c5710d35ddadfcf95c362b0aa8de11ab3972bc4f230ebd58a6c49ee", size = 144483, upload-time = "2026-01-28T01:22:25.772Z" },
-]
-
-[[package]]
-name = "pytokens"
-version = "0.4.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e5/16/4b9cfd90d55e66ffdb277d7ebe3bc25250c2311336ec3fc73b2673c794d5/pytokens-0.4.0.tar.gz", hash = "sha256:6b0b03e6ea7c9f9d47c5c61164b69ad30f4f0d70a5d9fe7eac4d19f24f77af2d", size = 15039, upload-time = "2026-01-19T07:59:50.623Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b4/05/3196399a353dd4cd99138a88f662810979ee2f1a1cdb0b417cb2f4507836/pytokens-0.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:92eb3ef88f27c22dc9dbab966ace4d61f6826e02ba04dac8e2d65ea31df56c8e", size = 160075, upload-time = "2026-01-19T07:59:00.316Z" },
- { url = "https://files.pythonhosted.org/packages/28/1d/c8fc4ed0a1c4f660391b201cda00b1d5bbcc00e2998e8bcd48b15eefd708/pytokens-0.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4b77858a680635ee9904306f54b0ee4781effb89e211ba0a773d76539537165", size = 247318, upload-time = "2026-01-19T07:59:01.636Z" },
- { url = "https://files.pythonhosted.org/packages/8e/0e/53e55ba01f3e858d229cd84b02481542f42ba59050483a78bf2447ee1af7/pytokens-0.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25cacc20c2ad90acb56f3739d87905473c54ca1fa5967ffcd675463fe965865e", size = 259752, upload-time = "2026-01-19T07:59:04.229Z" },
- { url = "https://files.pythonhosted.org/packages/dc/56/2d930d7f899e3f21868ca6e8ec739ac31e8fc532f66e09cbe45d3df0a84f/pytokens-0.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:628fab535ebc9079e4db35cd63cb401901c7ce8720a9834f9ad44b9eb4e0f1d4", size = 262842, upload-time = "2026-01-19T07:59:06.14Z" },
- { url = "https://files.pythonhosted.org/packages/42/dd/4e7e6920d23deffaf66e6f40d45f7610dcbc132ca5d90ab4faccef22f624/pytokens-0.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:4d0f568d7e82b7e96be56d03b5081de40e43c904eb6492bf09aaca47cd55f35b", size = 102620, upload-time = "2026-01-19T07:59:07.839Z" },
- { url = "https://files.pythonhosted.org/packages/3d/65/65460ebbfefd0bc1b160457904370d44f269e6e4582e0a9b6cba7c267b04/pytokens-0.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cd8da894e5a29ba6b6da8be06a4f7589d7220c099b5e363cb0643234b9b38c2a", size = 159864, upload-time = "2026-01-19T07:59:08.908Z" },
- { url = "https://files.pythonhosted.org/packages/25/70/a46669ec55876c392036b4da9808b5c3b1c5870bbca3d4cc923bf68bdbc1/pytokens-0.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:237ba7cfb677dbd3b01b09860810aceb448871150566b93cd24501d5734a04b1", size = 254448, upload-time = "2026-01-19T07:59:10.594Z" },
- { url = "https://files.pythonhosted.org/packages/62/0b/c486fc61299c2fc3b7f88ee4e115d4c8b6ffd1a7f88dc94b398b5b1bc4b8/pytokens-0.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01d1a61e36812e4e971cfe2c0e4c1f2d66d8311031dac8bf168af8a249fa04dd", size = 268863, upload-time = "2026-01-19T07:59:12.31Z" },
- { url = "https://files.pythonhosted.org/packages/79/92/b036af846707d25feaff7cafbd5280f1bd6a1034c16bb06a7c910209c1ab/pytokens-0.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e47e2ef3ec6ee86909e520d79f965f9b23389fda47460303cf715d510a6fe544", size = 267181, upload-time = "2026-01-19T07:59:13.856Z" },
- { url = "https://files.pythonhosted.org/packages/0d/c0/6d011fc00fefa74ce34816c84a923d2dd7c46b8dbc6ee52d13419786834c/pytokens-0.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d36954aba4557fd5a418a03cf595ecbb1cdcce119f91a49b19ef09d691a22ae", size = 102814, upload-time = "2026-01-19T07:59:15.288Z" },
- { url = "https://files.pythonhosted.org/packages/7c/3c/6941a82f4f130af6e1c68c076b6789069ef10c04559bd4733650f902fd3b/pytokens-0.4.0-py3-none-any.whl", hash = "sha256:0508d11b4de157ee12063901603be87fb0253e8f4cb9305eb168b1202ab92068", size = 13224, upload-time = "2026-01-19T07:59:49.822Z" },
-]
-
-[[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" },
-]
-
-[[package]]
-name = "regex"
-version = "2026.1.15"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d0/c9/0c80c96eab96948363d270143138d671d5731c3a692b417629bf3492a9d6/regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a", size = 488168, upload-time = "2026-01-14T23:14:16.129Z" },
- { url = "https://files.pythonhosted.org/packages/17/f0/271c92f5389a552494c429e5cc38d76d1322eb142fb5db3c8ccc47751468/regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f", size = 290636, upload-time = "2026-01-14T23:14:17.715Z" },
- { url = "https://files.pythonhosted.org/packages/a0/f9/5f1fd077d106ca5655a0f9ff8f25a1ab55b92128b5713a91ed7134ff688e/regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1", size = 288496, upload-time = "2026-01-14T23:14:19.326Z" },
- { url = "https://files.pythonhosted.org/packages/b5/e1/8f43b03a4968c748858ec77f746c286d81f896c2e437ccf050ebc5d3128c/regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b", size = 793503, upload-time = "2026-01-14T23:14:20.922Z" },
- { url = "https://files.pythonhosted.org/packages/8d/4e/a39a5e8edc5377a46a7c875c2f9a626ed3338cb3bb06931be461c3e1a34a/regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8", size = 860535, upload-time = "2026-01-14T23:14:22.405Z" },
- { url = "https://files.pythonhosted.org/packages/dc/1c/9dce667a32a9477f7a2869c1c767dc00727284a9fa3ff5c09a5c6c03575e/regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413", size = 907225, upload-time = "2026-01-14T23:14:23.897Z" },
- { url = "https://files.pythonhosted.org/packages/a4/3c/87ca0a02736d16b6262921425e84b48984e77d8e4e572c9072ce96e66c30/regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026", size = 800526, upload-time = "2026-01-14T23:14:26.039Z" },
- { url = "https://files.pythonhosted.org/packages/4b/ff/647d5715aeea7c87bdcbd2f578f47b415f55c24e361e639fe8c0cc88878f/regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785", size = 773446, upload-time = "2026-01-14T23:14:28.109Z" },
- { url = "https://files.pythonhosted.org/packages/af/89/bf22cac25cb4ba0fe6bff52ebedbb65b77a179052a9d6037136ae93f42f4/regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e", size = 783051, upload-time = "2026-01-14T23:14:29.929Z" },
- { url = "https://files.pythonhosted.org/packages/1e/f4/6ed03e71dca6348a5188363a34f5e26ffd5db1404780288ff0d79513bce4/regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763", size = 854485, upload-time = "2026-01-14T23:14:31.366Z" },
- { url = "https://files.pythonhosted.org/packages/d9/9a/8e8560bd78caded8eb137e3e47612430a05b9a772caf60876435192d670a/regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb", size = 762195, upload-time = "2026-01-14T23:14:32.802Z" },
- { url = "https://files.pythonhosted.org/packages/38/6b/61fc710f9aa8dfcd764fe27d37edfaa023b1a23305a0d84fccd5adb346ea/regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2", size = 845986, upload-time = "2026-01-14T23:14:34.898Z" },
- { url = "https://files.pythonhosted.org/packages/fd/2e/fbee4cb93f9d686901a7ca8d94285b80405e8c34fe4107f63ffcbfb56379/regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1", size = 788992, upload-time = "2026-01-14T23:14:37.116Z" },
- { url = "https://files.pythonhosted.org/packages/ed/14/3076348f3f586de64b1ab75a3fbabdaab7684af7f308ad43be7ef1849e55/regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569", size = 265893, upload-time = "2026-01-14T23:14:38.426Z" },
- { url = "https://files.pythonhosted.org/packages/0f/19/772cf8b5fc803f5c89ba85d8b1870a1ca580dc482aa030383a9289c82e44/regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7", size = 277840, upload-time = "2026-01-14T23:14:39.785Z" },
- { url = "https://files.pythonhosted.org/packages/78/84/d05f61142709474da3c0853222d91086d3e1372bcdab516c6fd8d80f3297/regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec", size = 270374, upload-time = "2026-01-14T23:14:41.592Z" },
- { url = "https://files.pythonhosted.org/packages/92/81/10d8cf43c807d0326efe874c1b79f22bfb0fb226027b0b19ebc26d301408/regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1", size = 489398, upload-time = "2026-01-14T23:14:43.741Z" },
- { url = "https://files.pythonhosted.org/packages/90/b0/7c2a74e74ef2a7c32de724658a69a862880e3e4155cba992ba04d1c70400/regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681", size = 291339, upload-time = "2026-01-14T23:14:45.183Z" },
- { url = "https://files.pythonhosted.org/packages/19/4d/16d0773d0c818417f4cc20aa0da90064b966d22cd62a8c46765b5bd2d643/regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f", size = 289003, upload-time = "2026-01-14T23:14:47.25Z" },
- { url = "https://files.pythonhosted.org/packages/c6/e4/1fc4599450c9f0863d9406e944592d968b8d6dfd0d552a7d569e43bceada/regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa", size = 798656, upload-time = "2026-01-14T23:14:48.77Z" },
- { url = "https://files.pythonhosted.org/packages/b2/e6/59650d73a73fa8a60b3a590545bfcf1172b4384a7df2e7fe7b9aab4e2da9/regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804", size = 864252, upload-time = "2026-01-14T23:14:50.528Z" },
- { url = "https://files.pythonhosted.org/packages/6e/ab/1d0f4d50a1638849a97d731364c9a80fa304fec46325e48330c170ee8e80/regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c", size = 912268, upload-time = "2026-01-14T23:14:52.952Z" },
- { url = "https://files.pythonhosted.org/packages/dd/df/0d722c030c82faa1d331d1921ee268a4e8fb55ca8b9042c9341c352f17fa/regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5", size = 803589, upload-time = "2026-01-14T23:14:55.182Z" },
- { url = "https://files.pythonhosted.org/packages/66/23/33289beba7ccb8b805c6610a8913d0131f834928afc555b241caabd422a9/regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3", size = 775700, upload-time = "2026-01-14T23:14:56.707Z" },
- { url = "https://files.pythonhosted.org/packages/e7/65/bf3a42fa6897a0d3afa81acb25c42f4b71c274f698ceabd75523259f6688/regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb", size = 787928, upload-time = "2026-01-14T23:14:58.312Z" },
- { url = "https://files.pythonhosted.org/packages/f4/f5/13bf65864fc314f68cdd6d8ca94adcab064d4d39dbd0b10fef29a9da48fc/regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410", size = 858607, upload-time = "2026-01-14T23:15:00.657Z" },
- { url = "https://files.pythonhosted.org/packages/a3/31/040e589834d7a439ee43fb0e1e902bc81bd58a5ba81acffe586bb3321d35/regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4", size = 763729, upload-time = "2026-01-14T23:15:02.248Z" },
- { url = "https://files.pythonhosted.org/packages/9b/84/6921e8129687a427edf25a34a5594b588b6d88f491320b9de5b6339a4fcb/regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d", size = 850697, upload-time = "2026-01-14T23:15:03.878Z" },
- { url = "https://files.pythonhosted.org/packages/8a/87/3d06143d4b128f4229158f2de5de6c8f2485170c7221e61bf381313314b2/regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22", size = 789849, upload-time = "2026-01-14T23:15:06.102Z" },
- { url = "https://files.pythonhosted.org/packages/77/69/c50a63842b6bd48850ebc7ab22d46e7a2a32d824ad6c605b218441814639/regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913", size = 266279, upload-time = "2026-01-14T23:15:07.678Z" },
- { url = "https://files.pythonhosted.org/packages/f2/36/39d0b29d087e2b11fd8191e15e81cce1b635fcc845297c67f11d0d19274d/regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a", size = 277166, upload-time = "2026-01-14T23:15:09.257Z" },
- { url = "https://files.pythonhosted.org/packages/28/32/5b8e476a12262748851fa8ab1b0be540360692325975b094e594dfebbb52/regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056", size = 270415, upload-time = "2026-01-14T23:15:10.743Z" },
-]
-
-[[package]]
-name = "requests"
-version = "2.32.5"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "certifi" },
- { name = "charset-normalizer" },
- { name = "idna" },
- { name = "urllib3" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
-]
-
-[[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 = "responses"
-version = "0.25.8"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pyyaml" },
- { name = "requests" },
- { name = "urllib3" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/0e/95/89c054ad70bfef6da605338b009b2e283485835351a9935c7bfbfaca7ffc/responses-0.25.8.tar.gz", hash = "sha256:9374d047a575c8f781b94454db5cab590b6029505f488d12899ddb10a4af1cf4", size = 79320, upload-time = "2025-08-08T19:01:46.709Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/1c/4c/cc276ce57e572c102d9542d383b2cfd551276581dc60004cb94fe8774c11/responses-0.25.8-py3-none-any.whl", hash = "sha256:0c710af92def29c8352ceadff0c3fe340ace27cf5af1bbe46fb71275bcd2831c", size = 34769, upload-time = "2025-08-08T19:01:45.018Z" },
-]
-
-[[package]]
-name = "rich"
-version = "14.3.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "markdown-it-py" },
- { name = "pygments" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/a1/84/4831f881aa6ff3c976f6d6809b58cdfa350593ffc0dc3c58f5f6586780fb/rich-14.3.1.tar.gz", hash = "sha256:b8c5f568a3a749f9290ec6bddedf835cec33696bfc1e48bcfecb276c7386e4b8", size = 230125, upload-time = "2026-01-24T21:40:44.847Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/87/2a/a1810c8627b9ec8c57ec5ec325d306701ae7be50235e8fd81266e002a3cc/rich-14.3.1-py3-none-any.whl", hash = "sha256:da750b1aebbff0b372557426fb3f35ba56de8ef954b3190315eb64076d6fb54e", size = 309952, upload-time = "2026-01-24T21:40:42.969Z" },
-]
-
-[[package]]
-name = "rsa"
-version = "4.9.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pyasn1" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
-]
-
-[[package]]
-name = "s3transfer"
-version = "0.16.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "botocore" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" },
-]
-
-[[package]]
-name = "sentry-sdk"
-version = "2.51.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "certifi" },
- { name = "urllib3" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/6f/9f/094bbb6be5cf218ab6712c6528310687f3d3fe8818249fcfe1d74192f7c5/sentry_sdk-2.51.0.tar.gz", hash = "sha256:b89d64577075fd8c13088bc3609a2ce77a154e5beb8cba7cc16560b0539df4f7", size = 407447, upload-time = "2026-01-28T10:29:50.962Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a0/da/df379404d484ca9dede4ad8abead5de828cdcff35623cd44f0351cf6869c/sentry_sdk-2.51.0-py2.py3-none-any.whl", hash = "sha256:e21016d318a097c2b617bb980afd9fc737e1efc55f9b4f0cdc819982c9717d5f", size = 431426, upload-time = "2026-01-28T10:29:48.868Z" },
-]
-
-[[package]]
-name = "shellingham"
-version = "1.5.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
-]
-
-[[package]]
-name = "six"
-version = "1.17.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
-]
-
-[[package]]
-name = "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 = "syrupy"
-version = "5.1.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pytest" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/2e/b0/24bca682d6a6337854be37f242d116cceeda9942571d5804c44bc1bdd427/syrupy-5.1.0.tar.gz", hash = "sha256:df543c7aa50d3cf1246e83d58fe490afe5f7dab7b41e74ecc0d8d23ae19bd4b8", size = 50495, upload-time = "2026-01-25T14:53:06.2Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/de/70/cf880c3b95a6034ef673e74b369941b42315c01f1554a5637a4f8b911009/syrupy-5.1.0-py3-none-any.whl", hash = "sha256:95162d2b05e61ed3e13f117b88dfab7c58bd6f90e66ebbf918e8a77114ad51c5", size = 51658, upload-time = "2026-01-25T14:53:05.105Z" },
-]
-
-[[package]]
-name = "tabulate"
-version = "0.9.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" },
-]
-
-[[package]]
-name = "tblib"
-version = "2.0.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/64/e3/d9aebe40d15d2c4c73a0ff8555326ef42a62ce3e5320ceb1aa762e4fbb54/tblib-2.0.0.tar.gz", hash = "sha256:a6df30f272c08bf8be66e0775fad862005d950a6b8449b94f7c788731d70ecd7", size = 28695, upload-time = "2023-06-22T08:24:16.494Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9e/bd/ccb241b97e39dd8ec143418f89b3fd5752f872c862877a0f1b2d9fb9e815/tblib-2.0.0-py3-none-any.whl", hash = "sha256:9100bfa016b047d5b980d66e7efed952fbd20bd85b56110aaf473cb97d18709a", size = 11455, upload-time = "2023-06-22T08:24:14.248Z" },
-]
-
-[[package]]
-name = "tenacity"
-version = "9.1.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" },
-]
-
-[[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" },
-]
-
-[[package]]
-name = "together"
-version = "1.5.35"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "aiohttp" },
- { name = "black" },
- { name = "click" },
- { name = "eval-type-backport" },
- { name = "filelock" },
- { name = "numpy" },
- { name = "pillow" },
- { name = "pydantic" },
- { name = "requests" },
- { name = "rich" },
- { name = "tabulate" },
- { name = "tqdm" },
- { name = "typer" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/5a/1d/6c50e0e32af097d966723e63b8e5ee02cfb002a40b6095c8ac65d6c08fe8/together-1.5.35.tar.gz", hash = "sha256:db3fc7dbc04dca044f437cd28224432e17567e6650dc1afd09780b48c0187cff", size = 91037, upload-time = "2026-01-21T23:15:15.909Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/68/e6/cd079d92f9eab83cd48c932e9b1e5e7fe25d90576f913dde66373135c392/together-1.5.35-py3-none-any.whl", hash = "sha256:74b6192e26492dbce2570fb801f884e74739bae1045b20c5b070a71639d7d5fc", size = 120461, upload-time = "2026-01-21T23:15:14.054Z" },
-]
-
-[[package]]
-name = "toml"
-version = "0.10.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" },
-]
-
-[[package]]
-name = "tomlkit"
-version = "0.14.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" },
-]
-
-[[package]]
-name = "tqdm"
-version = "4.67.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
-]
-
-[[package]]
-name = "traceback-with-variables"
-version = "2.2.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/34/b1/25ee53be3125145cef9385f159a44547b4a01e1b2d2828055ca69b7e18aa/traceback_with_variables-2.2.1.tar.gz", hash = "sha256:ea7c695f9b401762f68f75df0439d661112b8dbd58bcd6910e402cff925ad7e0", size = 26145, upload-time = "2025-10-24T13:39:35.141Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/4a/06/fda9970d55fbbb7cd5cc856da2a9c693107e20f84386596da1f25b90a8cf/traceback_with_variables-2.2.1-py3-none-any.whl", hash = "sha256:ab6d75c72d26d61217962d11db44c98c62dccd2fedb2d4fb0ae4f9faf9db23c2", size = 22388, upload-time = "2025-10-24T13:29:32.712Z" },
-]
-
-[[package]]
-name = "typeid-python"
-version = "0.3.9"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "uuid-utils" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/ac/83/d1b140e4941a05ab7f4ccc2a5466b37fa559f48a9d3684d8107a7511508f/typeid_python-0.3.9.tar.gz", hash = "sha256:7cf7ede21e6ba8f272981dbae504d1256261d03edab42fb05d36787dbfd589bd", size = 25201, upload-time = "2026-01-28T18:23:38.917Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/5b/30/523b728eab3157d818818ac022579b37b2d5f7994eb6e2bb9636a08a712a/typeid_python-0.3.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c37992e3aeee2ca2d1c5412a8a1bbbff71454c7db30b343a40ac2ab7ffe0d892", size = 241454, upload-time = "2026-01-28T18:23:14.452Z" },
- { url = "https://files.pythonhosted.org/packages/e0/da/13d577f76f5fceb77dbdc94b8435d86fbbbe472d5fa86245a2ece22ece20/typeid_python-0.3.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b72f9e75eb8ae229e56c00a25330fbb9881f76f9b3a67fd3e35470328db1078", size = 238088, upload-time = "2026-01-28T18:23:16.429Z" },
- { url = "https://files.pythonhosted.org/packages/4d/36/a81e397a341d51315288835a809c5038835f925da00d11dce3e10115693f/typeid_python-0.3.9-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:b035ef63c243776783551963983d12146b134c52475edfc42a33ce0a9baea337", size = 271957, upload-time = "2026-01-28T18:23:18.236Z" },
- { url = "https://files.pythonhosted.org/packages/03/06/dd85db0de10c6337e3e9fc457fda009f4cdac83fa873418c7e19c6041ecb/typeid_python-0.3.9-cp311-cp311-win_amd64.whl", hash = "sha256:751b0aaeeb5c8c0bd87d15c77ffe52df86c372d66572e6b2ec3fb2df056d790e", size = 132071, upload-time = "2026-01-28T18:23:19.583Z" },
- { url = "https://files.pythonhosted.org/packages/6c/7b/bb76c6016f862b9cb5a4e7d6cb3d1378cc342793644ba6a2022f05790dc6/typeid_python-0.3.9-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:fc193d8a17529c65915273a9734e7d724b8b279450d01b2559a77b8bd53f52da", size = 240430, upload-time = "2026-01-28T18:23:21.506Z" },
- { url = "https://files.pythonhosted.org/packages/20/0c/f8834a0d465afc4ea194d43fe10dd11e23874f5c102cec260172108a5cdc/typeid_python-0.3.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7dedb9894dea2a39653822224af6292710a54af0c9f48ba896bea6c22b6bff06", size = 236578, upload-time = "2026-01-28T18:23:22.874Z" },
- { url = "https://files.pythonhosted.org/packages/b1/29/c9842c47610213821cddbb274d093e36dc5d4346195eb5f036856f7d15f3/typeid_python-0.3.9-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:20ce022fe7d6be11a0d332d794e75a056b5aa225969f4b8ef9dbe4a94e651f2c", size = 270059, upload-time = "2026-01-28T18:23:24.396Z" },
- { url = "https://files.pythonhosted.org/packages/8b/87/9c3ae9936491f29ff09dcdbfdc4476368cc75b09be515ab31c5a2519ea7d/typeid_python-0.3.9-cp312-cp312-win_amd64.whl", hash = "sha256:bba27b8d1708e9654b85ffbcc4fc89a7c1d6cea65fbdab7fb9069d8cd71c2acd", size = 131062, upload-time = "2026-01-28T18:23:25.832Z" },
-]
-
-[[package]]
-name = "typer"
-version = "0.19.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "click" },
- { name = "rich" },
- { name = "shellingham" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/21/ca/950278884e2ca20547ff3eb109478c6baf6b8cf219318e6bc4f666fad8e8/typer-0.19.2.tar.gz", hash = "sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca", size = 104755, upload-time = "2025-09-23T09:47:48.256Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/00/22/35617eee79080a5d071d0f14ad698d325ee6b3bf824fc0467c03b30e7fa8/typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9", size = 46748, upload-time = "2025-09-23T09:47:46.777Z" },
-]
-
-[[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.6.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
-]
-
-[[package]]
-name = "uuid-utils"
-version = "0.14.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/57/7c/3a926e847516e67bc6838634f2e54e24381105b4e80f9338dc35cca0086b/uuid_utils-0.14.0.tar.gz", hash = "sha256:fc5bac21e9933ea6c590433c11aa54aaca599f690c08069e364eb13a12f670b4", size = 22072, upload-time = "2026-01-20T20:37:15.729Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a7/42/42d003f4a99ddc901eef2fd41acb3694163835e037fb6dde79ad68a72342/uuid_utils-0.14.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f6695c0bed8b18a904321e115afe73b34444bc8451d0ce3244a1ec3b84deb0e5", size = 601786, upload-time = "2026-01-20T20:37:09.843Z" },
- { url = "https://files.pythonhosted.org/packages/96/e6/775dfb91f74b18f7207e3201eb31ee666d286579990dc69dd50db2d92813/uuid_utils-0.14.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:4f0a730bbf2d8bb2c11b93e1005e91769f2f533fa1125ed1f00fd15b6fcc732b", size = 303943, upload-time = "2026-01-20T20:37:18.767Z" },
- { url = "https://files.pythonhosted.org/packages/17/82/ea5f5e85560b08a1f30cdc65f75e76494dc7aba9773f679e7eaa27370229/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40ce3fd1a4fdedae618fc3edc8faf91897012469169d600133470f49fd699ed3", size = 340467, upload-time = "2026-01-20T20:37:11.794Z" },
- { url = "https://files.pythonhosted.org/packages/ca/33/54b06415767f4569882e99b6470c6c8eeb97422686a6d432464f9967fd91/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:09ae4a98416a440e78f7d9543d11b11cae4bab538b7ed94ec5da5221481748f2", size = 346333, upload-time = "2026-01-20T20:37:12.818Z" },
- { url = "https://files.pythonhosted.org/packages/cb/10/a6bce636b8f95e65dc84bf4a58ce8205b8e0a2a300a38cdbc83a3f763d27/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:971e8c26b90d8ae727e7f2ac3ee23e265971d448b3672882f2eb44828b2b8c3e", size = 470859, upload-time = "2026-01-20T20:37:01.512Z" },
- { url = "https://files.pythonhosted.org/packages/8a/27/84121c51ea72f013f0e03d0886bcdfa96b31c9b83c98300a7bd5cc4fa191/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5cde1fa82804a8f9d2907b7aec2009d440062c63f04abbdb825fce717a5e860", size = 341988, upload-time = "2026-01-20T20:37:22.881Z" },
- { url = "https://files.pythonhosted.org/packages/90/a4/01c1c7af5e6a44f20b40183e8dac37d6ed83e7dc9e8df85370a15959b804/uuid_utils-0.14.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7343862a2359e0bd48a7f3dfb5105877a1728677818bb694d9f40703264a2db", size = 365784, upload-time = "2026-01-20T20:37:10.808Z" },
- { url = "https://files.pythonhosted.org/packages/04/f0/65ee43ec617b8b6b1bf2a5aecd56a069a08cca3d9340c1de86024331bde3/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c51e4818fdb08ccec12dc7083a01f49507b4608770a0ab22368001685d59381b", size = 523750, upload-time = "2026-01-20T20:37:06.152Z" },
- { url = "https://files.pythonhosted.org/packages/95/d3/6bf503e3f135a5dfe705a65e6f89f19bccd55ac3fb16cb5d3ec5ba5388b8/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:181bbcccb6f93d80a8504b5bd47b311a1c31395139596edbc47b154b0685b533", size = 615818, upload-time = "2026-01-20T20:37:21.816Z" },
- { url = "https://files.pythonhosted.org/packages/df/6c/99937dd78d07f73bba831c8dc9469dfe4696539eba2fc269ae1b92752f9e/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:5c8ae96101c3524ba8dbf762b6f05e9e9d896544786c503a727c5bf5cb9af1a7", size = 580831, upload-time = "2026-01-20T20:37:19.691Z" },
- { url = "https://files.pythonhosted.org/packages/44/fa/bbc9e2c25abd09a293b9b097a0d8fc16acd6a92854f0ec080f1ea7ad8bb3/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00ac3c6edfdaff7e1eed041f4800ae09a3361287be780d7610a90fdcde9befdc", size = 546333, upload-time = "2026-01-20T20:37:03.117Z" },
- { url = "https://files.pythonhosted.org/packages/e7/9b/e5e99b324b1b5f0c62882230455786df0bc66f67eff3b452447e703f45d2/uuid_utils-0.14.0-cp39-abi3-win32.whl", hash = "sha256:ec2fd80adf8e0e6589d40699e6f6df94c93edcc16dd999be0438dd007c77b151", size = 177319, upload-time = "2026-01-20T20:37:04.208Z" },
- { url = "https://files.pythonhosted.org/packages/d3/28/2c7d417ea483b6ff7820c948678fdf2ac98899dc7e43bb15852faa95acaf/uuid_utils-0.14.0-cp39-abi3-win_amd64.whl", hash = "sha256:efe881eb43a5504fad922644cb93d725fd8a6a6d949bd5a4b4b7d1a1587c7fd1", size = 182566, upload-time = "2026-01-20T20:37:16.868Z" },
- { url = "https://files.pythonhosted.org/packages/b8/86/49e4bdda28e962fbd7266684171ee29b3d92019116971d58783e51770745/uuid_utils-0.14.0-cp39-abi3-win_arm64.whl", hash = "sha256:32b372b8fd4ebd44d3a219e093fe981af4afdeda2994ee7db208ab065cfcd080", size = 182809, upload-time = "2026-01-20T20:37:05.139Z" },
- { url = "https://files.pythonhosted.org/packages/f1/03/1f1146e32e94d1f260dfabc81e1649102083303fb4ad549775c943425d9a/uuid_utils-0.14.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:762e8d67992ac4d2454e24a141a1c82142b5bde10409818c62adbe9924ebc86d", size = 587430, upload-time = "2026-01-20T20:37:24.998Z" },
- { url = "https://files.pythonhosted.org/packages/87/ba/d5a7469362594d885fd9219fe9e851efbe65101d3ef1ef25ea321d7ce841/uuid_utils-0.14.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:40be5bf0b13aa849d9062abc86c198be6a25ff35316ce0b89fc25f3bac6d525e", size = 298106, upload-time = "2026-01-20T20:37:23.896Z" },
- { url = "https://files.pythonhosted.org/packages/8a/11/3dafb2a5502586f59fd49e93f5802cd5face82921b3a0f3abb5f357cb879/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:191a90a6f3940d1b7322b6e6cceff4dd533c943659e0a15f788674407856a515", size = 333423, upload-time = "2026-01-20T20:37:17.828Z" },
- { url = "https://files.pythonhosted.org/packages/7c/f2/c8987663f0cdcf4d717a36d85b5db2a5589df0a4e129aa10f16f4380ef48/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4aa4525f4ad82f9d9c842f9a3703f1539c1808affbaec07bb1b842f6b8b96aa5", size = 338659, upload-time = "2026-01-20T20:37:14.286Z" },
- { url = "https://files.pythonhosted.org/packages/d1/c8/929d81665d83f0b2ffaecb8e66c3091a50f62c7cb5b65e678bd75a96684e/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdbd82ff20147461caefc375551595ecf77ebb384e46267f128aca45a0f2cdfc", size = 467029, upload-time = "2026-01-20T20:37:08.277Z" },
- { url = "https://files.pythonhosted.org/packages/8e/a0/27d7daa1bfed7163f4ccaf52d7d2f4ad7bb1002a85b45077938b91ee584f/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff57e8a5d540006ce73cf0841a643d445afe78ba12e75ac53a95ca2924a56be", size = 333298, upload-time = "2026-01-20T20:37:07.271Z" },
- { url = "https://files.pythonhosted.org/packages/63/d4/acad86ce012b42ce18a12f31ee2aa3cbeeb98664f865f05f68c882945913/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3fd9112ca96978361201e669729784f26c71fecc9c13a7f8a07162c31bd4d1e2", size = 359217, upload-time = "2026-01-20T20:36:59.687Z" },
-]
-
-[[package]]
-name = "websockets"
-version = "15.0.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" },
- { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" },
- { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" },
- { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" },
- { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" },
- { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" },
- { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" },
- { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" },
- { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" },
- { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" },
- { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" },
- { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" },
- { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" },
- { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" },
- { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" },
- { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" },
- { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" },
- { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" },
- { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" },
- { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" },
- { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" },
- { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" },
- { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
-]
-
-[[package]]
-name = "werkzeug"
-version = "3.1.5"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "markupsafe" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" },
-]
-
-[[package]]
-name = "win32-setctime"
-version = "1.2.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" },
-]
-
-[[package]]
-name = "xmltodict"
-version = "1.0.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/6a/aa/917ceeed4dbb80d2f04dbd0c784b7ee7bba8ae5a54837ef0e5e062cd3cfb/xmltodict-1.0.2.tar.gz", hash = "sha256:54306780b7c2175a3967cad1db92f218207e5bc1aba697d887807c0fb68b7649", size = 25725, upload-time = "2025-09-17T21:59:26.459Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c0/20/69a0e6058bc5ea74892d089d64dfc3a62ba78917ec5e2cfa70f7c92ba3a5/xmltodict-1.0.2-py3-none-any.whl", hash = "sha256:62d0fddb0dcbc9f642745d8bbf4d81fd17d6dfaec5a15b5c1876300aad92af0d", size = 13893, upload-time = "2025-09-17T21:59:24.859Z" },
-]
-
-[[package]]
-name = "yarl"
-version = "1.22.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "idna" },
- { name = "multidict" },
- { name = "propcache" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" },
- { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" },
- { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" },
- { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" },
- { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" },
- { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" },
- { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" },
- { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" },
- { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" },
- { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" },
- { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" },
- { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" },
- { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" },
- { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" },
- { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" },
- { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" },
- { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" },
- { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" },
- { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" },
- { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" },
- { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" },
- { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" },
- { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" },
- { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" },
- { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" },
- { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" },
- { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" },
- { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" },
- { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" },
- { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" },
- { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" },
- { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" },
- { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" },
-]
-
-[[package]]
-name = "yasoo"
-version = "0.12.6"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "attrs" },
- { name = "more-itertools" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/cf/00/a1ed9035b00254227c684161c3d4037f767ed53a1993b69c00c9d4d94f25/yasoo-0.12.6.tar.gz", hash = "sha256:aec81e790045198e8f51f92353f11923580f1c94f49eed2b543f286e4cc1c5cc", size = 13705, upload-time = "2022-10-22T10:05:39.619Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/68/21/cfd73fd9cc69855bffdda55cbeefa45ffd415f97704f67ceda1eb57141ad/yasoo-0.12.6-py3-none-any.whl", hash = "sha256:7400ff055c2153d670c04c82621fd4aeafc3f1fc7c0f99240831752357b67e1f", size = 14850, upload-time = "2022-10-22T10:05:38.4Z" },
-]
diff --git a/imbue_tools/README.md b/imbue_tools/README.md
@@ -1,9 +0,0 @@
-# Purpose
-Shared functionality for CLI tools like vet.
-
-# Contents
-- formatting git repos as LLM input
-
-# Excluded files list
-We currently maintain a hard-coded list of filename patterns for which we'll only ever include the file name, rather than the files' contents, in the LLM input.
-This list is maintained in the EXCLUSIONS_PATHSPEC constant in imbue_tools/imbue_tools/repo_utils/context_prefix.py .
diff --git a/imbue_tools/imbue_tools/get_conversation_history/get_conversation_history.py b/imbue_tools/imbue_tools/get_conversation_history/get_conversation_history.py
@@ -1,70 +0,0 @@
-import json
-from typing import assert_never
-
-from loguru import logger
-from pydantic import TypeAdapter
-from pydantic import ValidationError
-
-from vet_types.chat_state import ContentBlockTypes
-from vet_types.messages import ChatInputUserMessage
-from vet_types.messages import ConversationMessageUnion
-from vet_types.messages import ResponseBlockAgentMessage
-
-
-class ConversationLoadingError(Exception):
- pass
-
-
-# === formatting for prompt ===
-
-
-def delete_unnecessary_content_block_fields(block: ContentBlockTypes) -> str:
- """Returns the content as a json-serialized string without the fields that we don't want to include in the prompt"""
- fields_to_remove = {"id"}
- return block.model_dump_json(exclude=fields_to_remove)
-
-
-def delete_unnecessary_conversation_message_fields(
- message: ConversationMessageUnion,
-) -> str:
- """Returns the message as a json-serialized string without the fields that we don't want to include in the prompt"""
- general_fields_to_remove = {"message_id", "source", "approximate_creation_time"}
- match message:
- case ChatInputUserMessage():
- # remove the 'files' field if it's empty
- fields_to_remove = general_fields_to_remove | {"model_name"} | {"files"} if not message.files else set()
- return message.model_dump_json(exclude=fields_to_remove)
- case ResponseBlockAgentMessage():
- fields_to_remove = general_fields_to_remove | {"assistant_message_id"}
- return json.dumps(
- message.model_dump(mode="json", exclude=fields_to_remove)
- | {"content": [delete_unnecessary_content_block_fields(block) for block in message.content]}
- )
- case _ as unreachable:
- assert_never(unreachable)
-
-
-def format_conversation_history_for_prompt(
- conversation_history: tuple[ConversationMessageUnion, ...],
-) -> str:
- formatted_messages = [delete_unnecessary_conversation_message_fields(message) for message in conversation_history]
- return "\n".join(message for message in formatted_messages if message is not None)
-
-
-# === loading from file ===
-
-
-def parse_conversation_history(
- conversation_str: str,
-) -> tuple[ConversationMessageUnion, ...]:
- """Load a jsonl string into a list of conversation messages"""
- messages = []
- for line in conversation_str.strip().splitlines():
- try:
- # deserialize the message with pydantic
- message: ConversationMessageUnion = TypeAdapter(ConversationMessageUnion).validate_json(line)
- except ValidationError:
- logger.info("Skipping malformed history line {}", line)
- continue
- messages.append(message)
- return tuple(messages)
diff --git a/imbue_tools/imbue_tools/get_conversation_history/input_data_types.py b/imbue_tools/imbue_tools/get_conversation_history/input_data_types.py
@@ -1,72 +0,0 @@
-from typing import Self
-from typing import TypeVar
-
-from pydantic import model_validator
-
-from imbue_core.pydantic_serialization import SerializableModel
-from vet_types.messages import ConversationMessageUnion
-
-
-class IdentifierInputsMissingError(Exception):
- pass
-
-
-class IdentifierInputs(SerializableModel):
- # goal (for now, commit message) and diff to check
- maybe_goal: str | None = None
- maybe_diff: str | None = None
-
- # whole files to check
- maybe_files: tuple[str, ...] | None = None
-
- # conversation history to check
- maybe_conversation_history: tuple[ConversationMessageUnion, ...] | None = None
-
-
-class CommitInputs(IdentifierInputs):
- # pyre-ignore[56]: pyre's stubs don't match pydantic v2 decorator signatures
- @model_validator(mode="after")
- def validate_goal_not_none(self) -> Self:
- if self.maybe_goal is None:
- raise IdentifierInputsMissingError("goal cannot be None for CommitInputs")
- return self
-
- # pyre-ignore[56]: pyre's stubs don't match pydantic v2 decorator signatures
- @model_validator(mode="after")
- def validate_diff_not_none(self) -> Self:
- if self.maybe_diff is None:
- raise IdentifierInputsMissingError("goal cannot be None for CommitInputs")
- return self
-
- @property
- def goal(self) -> str:
- assert self.maybe_goal is not None
- return self.maybe_goal
-
- @property
- def diff(self) -> str:
- assert self.maybe_diff is not None
- return self.maybe_diff
-
-
-class ConversationInputs(IdentifierInputs):
- # pyre-ignore[56]: pyre's stubs don't match pydantic v2 decorator signatures
- @model_validator(mode="after")
- def validate_conversation_history_not_none(self) -> Self:
- if self.maybe_conversation_history is None:
- raise IdentifierInputsMissingError("conversation_history is required for conversation inputs")
- return self
-
- @property
- def conversation_history(self) -> tuple[ConversationMessageUnion, ...]:
- assert self.maybe_conversation_history is not None
- return self.maybe_conversation_history
-
-
-SpecificIdentifierInputsType = TypeVar("SpecificIdentifierInputsType", bound=IdentifierInputs)
-
-
-def to_specific_inputs_type(
- identifier_inputs: IdentifierInputs, to_type: type[SpecificIdentifierInputsType]
-) -> SpecificIdentifierInputsType:
- return to_type(**identifier_inputs.model_dump())
diff --git a/imbue_tools/imbue_tools/llm_output_parsing/parse_model_json_response.py b/imbue_tools/imbue_tools/llm_output_parsing/parse_model_json_response.py
@@ -1,51 +0,0 @@
-import json
-import re
-from typing import TypeVar
-
-from pydantic import ValidationError
-
-from imbue_core.async_monkey_patches import log_exception
-from imbue_core.pydantic_serialization import SerializableModel
-
-
-def parse_json_block_from_response_text(response_text: str) -> str:
- """Clean markdown formatting and extra content from LLM response."""
- response_text = response_text.strip()
- # Parse content between first ```json and ``` block
- json_start_line = re.search(r"^.*?```json\s*", response_text, flags=re.MULTILINE)
- if json_start_line:
- response_text = response_text[json_start_line.end() :]
- json_end_line = re.search(r"```\s*$", response_text, flags=re.MULTILINE)
- if json_end_line:
- response_text = response_text[: json_end_line.start()]
- return response_text.strip()
-
-
-ResponseSchema = TypeVar("ResponseSchema", bound=SerializableModel)
-
-
-class ResponseParsingError(Exception):
- pass
-
-
-def parse_model_json_response(response_text: str, result_type: type[ResponseSchema]) -> ResponseSchema:
- """Parse a JSON response from the LLM into a Pydantic model."""
- cleaned_response = parse_json_block_from_response_text(response_text)
- try:
- return result_type.model_validate_json(cleaned_response)
- except json.JSONDecodeError as e:
- log_exception(
- e,
- "Response is not valid JSON.\nraw_response: {response_text}\ncleaned_response: {cleaned_response}",
- response_text=response_text,
- cleaned_response=cleaned_response,
- )
- raise ResponseParsingError(str(e)) from e
- except ValidationError as e:
- log_exception(
- e,
- "Response does not match the expected schema.\nraw_response: {response_text}\ncleaned_response: {cleaned_response}",
- response_text=response_text,
- cleaned_response=cleaned_response,
- )
- raise ResponseParsingError(str(e)) from e
diff --git a/imbue_tools/imbue_tools/repo_utils/context_prefix.py b/imbue_tools/imbue_tools/repo_utils/context_prefix.py
@@ -1,613 +0,0 @@
-import functools
-from enum import StrEnum
-from pathlib import Path
-from typing import Any
-from typing import Iterable
-from typing import Mapping
-from typing import assert_never
-
-from loguru import logger
-from pydantic import BaseModel
-from pydantic import ConfigDict
-
-from imbue_core.agents.configs import LanguageModelGenerationConfig
-from imbue_core.async_monkey_patches import log_exception
-from imbue_core.pydantic_serialization import SerializableModel
-from imbue_tools.repo_utils.context_utils import escape_prompt_markers
-from imbue_tools.repo_utils.context_utils import maybe_get_file_path_from_qualified_name
-from imbue_tools.repo_utils.data_types import FileContextUnion
-from imbue_tools.repo_utils.errors import ContextLengthExceededError
-from imbue_tools.repo_utils.file_system import InMemoryFileSystem
-from imbue_tools.repo_utils.python_imports import QualifiedName
-from imbue_tools.repo_utils.python_imports import STANDARD_LIBRARIES
-from imbue_tools.repo_utils.python_imports import get_global_imports
-from imbue_tools.repo_utils.subrepo_formatting import BaseFilenamePattern
-from imbue_tools.repo_utils.subrepo_formatting import ContextFormatStyle
-from imbue_tools.repo_utils.subrepo_formatting import ExactFilenamePattern
-from imbue_tools.repo_utils.subrepo_formatting import FilenamePattern
-from imbue_tools.repo_utils.subrepo_formatting import IntersectionFilenamePattern
-from imbue_tools.repo_utils.subrepo_formatting import NegatedFilenamePattern
-from imbue_tools.repo_utils.subrepo_formatting import REPO_CONTEXT_TEMPLATE
-from imbue_tools.repo_utils.subrepo_formatting import SubrepoContextMatchers
-from imbue_tools.repo_utils.subrepo_formatting import UnionFilenamePattern
-from imbue_tools.repo_utils.subrepo_formatting import compute_file_context_format_styles
-from imbue_tools.repo_utils.subrepo_formatting import format_subrepo_context
-from imbue_tools.repo_utils.subrepo_formatting import (
- parse_subrepo_context_matchers_from_toml,
-)
-
-
-class SubrepoContext(SerializableModel):
- repo_context_files: tuple[FileContextUnion, ...]
- subrepo_context_strategy_label: str
-
-
-class SubrepoContextWithFormattedContext(SubrepoContext):
- formatted_repo_context: str
-
-
-def is_qualified_name_from_stdlib(qualified_name: QualifiedName) -> bool:
- return qualified_name.top_level_name.value in STANDARD_LIBRARIES
-
-
-def get_immediate_first_party_import_paths_for_python_file(
- current_file_path: str, full_repo_contents_map: InMemoryFileSystem
-) -> set[str] | None:
- file_contents = full_repo_contents_map.get_text(current_file_path)
- if not file_contents or not current_file_path.endswith(".py"):
- return None
-
- try:
- global_imports = get_global_imports(file_contents)
- except SyntaxError as e:
- log_exception(
- e,
- "Failed to parse imports for {current_file_path}",
- current_file_path=current_file_path,
- )
- return None
-
- parent_names: set[QualifiedName] = set()
- for import_ in global_imports:
- parent_name = import_.qualified_name.parent_name
- parent_names.add(parent_name)
-
- imported_file_paths = set()
- all_file_paths = [Path(x) for x in full_repo_contents_map.text_files.keys()]
- for parent_name in parent_names:
- other_file_path = maybe_get_file_path_from_qualified_name(parent_name, all_file_paths)
- # if this doesn't exist it's likely not a first party import so we can ignore it
- if not other_file_path or is_qualified_name_from_stdlib(parent_name):
- continue
- imported_file_paths.add(str(other_file_path))
-
- return imported_file_paths
-
-
-FULL_REPO_PATHSPEC = BaseFilenamePattern.from_lines(["/**"])
-DOC_FILE_EXTENSIONS = [".md", ".txt"]
-DOC_PATHSPEC = BaseFilenamePattern.from_lines([f"**/*{ext}" for ext in DOC_FILE_EXTENSIONS])
-
-# Common files that we want to exclude since they can be large and are of low signal for issue identification.
-EXCLUSIONS_PATHSPEC = BaseFilenamePattern.from_lines(["uv.lock", "**/__snapshots__/**"])
-
-
-def escape_gitignore_pattern(path: str) -> str:
- """
- Escape a path into a GitIgnore pattern that matches exactly the path.
-
- GitWildMatchPattern assigns special meaning to the following characters, which need to be escaped:
- `*`, `?`, `[`, `]` and `\\`.
- At the beginning of a line, we additionally need to escape leading `#` and `!` characters,
- and at the end of a line we need to escape trailing ` ` (space) characters.
- For simplicity, we simply escape these characters everywhere, which should still work correctly.
- """
- return (
- path.replace("\\", "\\\\")
- .replace("*", "\\*")
- .replace("?", "\\?")
- .replace("[", "\\[")
- .replace("]", "\\]")
- .replace("#", "\\#")
- .replace("!", "\\!")
- .replace(" ", "\\ ")
- )
-
-
-def first_level_files_along_paths(file_paths: Iterable[str]) -> FilenamePattern:
- """
- Create a pathspec that matches all files along the given paths, but doesn't match adjacent directories.
- """
- # for each level in the path, we create an IntersectionFilenamePattern
- # which has one branch that matches everything starting with that path,
- # and another branch which matches everything except subdirectories starting with that path
- # then we OR these all together as a UnionFilenamePattern
- sorted_file_paths = sorted(file_paths)
- file_patterns = []
- for file_path in sorted_file_paths:
- for parent in Path(file_path).parents:
- escaped_parent = Path(escape_gitignore_pattern(str(parent)))
- match_all = BaseFilenamePattern.from_lines([str("/" / escaped_parent / "*")])
- match_except_subdirectories = NegatedFilenamePattern.build_from_positive_pattern(
- BaseFilenamePattern.from_lines([str("/" / escaped_parent / "*/*")])
- )
- file_patterns.append(IntersectionFilenamePattern(specs=(match_all, match_except_subdirectories)))
- return UnionFilenamePattern(specs=tuple(file_patterns))
-
-
-# cache this since it's reused across strategies
-@functools.lru_cache(maxsize=5)
-def make_docs_pathspec_along_paths(file_paths: frozenset[str]) -> FilenamePattern:
- """
- Create a pathspec that matches documentation files (.md, .txt) along each parent folder of the given file paths.
- """
- return IntersectionFilenamePattern(specs=(DOC_PATHSPEC, first_level_files_along_paths(file_paths=file_paths)))
-
-
-INSTRUCTIONS_PATHSPEC = BaseFilenamePattern.from_lines(["**/.claude.md", "**/CLAUDE.md", "**/AGENTS.md"])
-
-
-@functools.lru_cache(maxsize=5)
-def make_relevant_files_pathspec(file_paths: frozenset[str]) -> FilenamePattern:
- """
- Create a pathspec that matches the given file paths.
- """
- return ExactFilenamePattern(filenames=tuple(sorted(file_paths)))
-
-
-# cache this since it's reused across strategies
-@functools.lru_cache(maxsize=5)
-def make_instructions_pathspec_along_paths(
- file_paths: frozenset[str],
-) -> FilenamePattern:
- """
- Create a pathspec that matches instruction files (e.g. .claude.md, CLAUDE.md, AGENTS.md) along each parent folder of the given file paths.
-
- Should match a strict subset of make_docs_pathspec_along_paths.
- """
- return IntersectionFilenamePattern(
- specs=(
- INSTRUCTIONS_PATHSPEC,
- first_level_files_along_paths(file_paths=file_paths),
- )
- )
-
-
-# cache this since it's reused across strategies
-@functools.lru_cache(maxsize=5)
-def make_imports_pathspec_for_paths(
- file_paths: frozenset[str], full_repo_contents: InMemoryFileSystem
-) -> FilenamePattern:
- """
- Create a pathspec that matches Python files that are imported by the given file paths.
- """
- full_repo_python_file_contents_map = InMemoryFileSystem.build(
- {k: v for k, v in full_repo_contents.files.items() if k.endswith(".py")}
- )
-
- imported_file_paths = set()
- for file_path in file_paths:
- if file_path.endswith(".py"):
- # Include first party imports
- imported_paths = get_immediate_first_party_import_paths_for_python_file(
- file_path, full_repo_python_file_contents_map
- )
- if imported_paths:
- imported_file_paths.update(imported_paths)
-
- return ExactFilenamePattern(filenames=tuple(sorted(imported_file_paths)))
-
-
-class SubrepoContextStrategy(BaseModel):
- model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)
- label: str
- matchers: SubrepoContextMatchers
-
-
-class SubrepoContextStrategyType(StrEnum):
- # defaults if we have relevant files
- FULL_REPO_CONTENTS = "full repo contents"
- RELEVANT_WHOLE_FILES_IMPORTS_DOCS_AND_ELSEWHERE_FILENAME = (
- "relevant files + immediate imports + docs along relevant paths + filenames elsewhere"
- )
- RELEVANT_WHOLE_FILES_IMPORTS_DOCS = "relevant files + immediate imports + docs along relevant paths"
- RELEVANT_WHOLE_FILES_AND_RELEVANT_STUBBIFIED_IMPORTS_DOCS = (
- "relevant files + stubbified imports + docs along relevant paths"
- )
- RELEVANT_WHOLE_FILES_DOCS = "relevant files + docs along relevant paths"
- RELEVANT_WHOLE_FILES_INSTRUCTIONS = "relevant files + agent instructions along relevant paths"
- RELEVANT_WHOLE_FILES = "relevant files"
- RELEVANT_STUBBIFIED_FILES = "relevant stubbified files"
- NOTHING = "nothing"
-
- # defaults if we don't have relevant files (missing FULL_REPO_CONTENTS and NOTHING because they're already listed for if we do have relevant files)
- WHOLE_DOCS_AND_OTHERWISE_FILENAMES = "docs + filenames elsewhere"
- WHOLE_INSTRUCTIONS_AND_OTHERWISE_FILENAMES = "agent instructions + filenames elsewhere"
- WHOLE_INSTRUCTIONS = "agent instructions"
-
- # defaults for providing instruction files if we have relevant files
- WHOLE_DOCS = "docs"
- WHOLE_INSTRUCTIONS_AND_RELEVANT_DOCS = "agent instructions + relevant docs"
- RELEVANT_DOCS = "relevant docs"
- RELEVANT_INSTRUCTIONS = "relevant agent instructions"
-
- # custom
- CUSTOM = "custom"
-
-
-class StrategyMode(StrEnum):
- REGULAR = "regular"
- DOCS = "docs"
-
-
-class AvailableInfoMode(StrEnum):
- YES_FILES = "yes_files"
- NO_FILES = "no_files"
-
-
-DEFAULT_STRATEGY_TYPES: dict[tuple[StrategyMode, AvailableInfoMode], tuple[SubrepoContextStrategyType, ...]] = {
- (StrategyMode.REGULAR, AvailableInfoMode.YES_FILES): (
- SubrepoContextStrategyType.FULL_REPO_CONTENTS,
- SubrepoContextStrategyType.RELEVANT_WHOLE_FILES_IMPORTS_DOCS_AND_ELSEWHERE_FILENAME,
- SubrepoContextStrategyType.RELEVANT_WHOLE_FILES_IMPORTS_DOCS,
- SubrepoContextStrategyType.RELEVANT_WHOLE_FILES_AND_RELEVANT_STUBBIFIED_IMPORTS_DOCS,
- SubrepoContextStrategyType.RELEVANT_WHOLE_FILES_DOCS,
- SubrepoContextStrategyType.RELEVANT_WHOLE_FILES_INSTRUCTIONS,
- SubrepoContextStrategyType.RELEVANT_WHOLE_FILES,
- SubrepoContextStrategyType.RELEVANT_STUBBIFIED_FILES,
- SubrepoContextStrategyType.NOTHING,
- ),
- (StrategyMode.REGULAR, AvailableInfoMode.NO_FILES): (
- SubrepoContextStrategyType.FULL_REPO_CONTENTS,
- SubrepoContextStrategyType.WHOLE_DOCS_AND_OTHERWISE_FILENAMES,
- SubrepoContextStrategyType.WHOLE_INSTRUCTIONS_AND_OTHERWISE_FILENAMES,
- SubrepoContextStrategyType.WHOLE_INSTRUCTIONS,
- SubrepoContextStrategyType.NOTHING,
- ),
- (StrategyMode.DOCS, AvailableInfoMode.YES_FILES): (
- SubrepoContextStrategyType.WHOLE_DOCS,
- SubrepoContextStrategyType.WHOLE_INSTRUCTIONS_AND_RELEVANT_DOCS,
- SubrepoContextStrategyType.RELEVANT_DOCS,
- SubrepoContextStrategyType.RELEVANT_INSTRUCTIONS,
- # these don't have `nothing` strategies because having some user instructions is crucial for
- # the issue identifier which uses this, whereas the others can do ok with just a diff
- ),
- (StrategyMode.DOCS, AvailableInfoMode.NO_FILES): (
- SubrepoContextStrategyType.WHOLE_DOCS,
- SubrepoContextStrategyType.WHOLE_INSTRUCTIONS,
- ),
-}
-
-
-def build_strategy(
- strategy_type: SubrepoContextStrategyType,
- full_repo_contents: InMemoryFileSystem,
- relevant_file_paths: frozenset[str] | None,
-) -> SubrepoContextStrategy:
- match strategy_type:
- case SubrepoContextStrategyType.FULL_REPO_CONTENTS as s:
- return SubrepoContextStrategy(
- label=s,
- matchers=((ContextFormatStyle.FULL_FILE, FULL_REPO_PATHSPEC),),
- )
- case SubrepoContextStrategyType.RELEVANT_WHOLE_FILES_IMPORTS_DOCS_AND_ELSEWHERE_FILENAME as s:
- return SubrepoContextStrategy(
- label=s,
- matchers=(
- (
- ContextFormatStyle.FULL_FILE,
- make_relevant_files_pathspec(relevant_file_paths),
- ),
- (
- ContextFormatStyle.FULL_FILE,
- make_imports_pathspec_for_paths(relevant_file_paths, full_repo_contents),
- ),
- (
- ContextFormatStyle.FULL_FILE,
- make_docs_pathspec_along_paths(relevant_file_paths),
- ),
- (ContextFormatStyle.FILENAME_ONLY, FULL_REPO_PATHSPEC),
- ),
- )
- case SubrepoContextStrategyType.RELEVANT_WHOLE_FILES_IMPORTS_DOCS as s:
- return SubrepoContextStrategy(
- label=s,
- matchers=(
- (
- ContextFormatStyle.FULL_FILE,
- make_relevant_files_pathspec(relevant_file_paths),
- ),
- (
- ContextFormatStyle.FULL_FILE,
- make_imports_pathspec_for_paths(relevant_file_paths, full_repo_contents),
- ),
- (
- ContextFormatStyle.FULL_FILE,
- make_docs_pathspec_along_paths(relevant_file_paths),
- ),
- (ContextFormatStyle.HIDDEN, FULL_REPO_PATHSPEC),
- ),
- )
- case SubrepoContextStrategyType.RELEVANT_WHOLE_FILES_AND_RELEVANT_STUBBIFIED_IMPORTS_DOCS as s:
- return SubrepoContextStrategy(
- label=s,
- matchers=(
- (
- ContextFormatStyle.FULL_FILE,
- make_relevant_files_pathspec(relevant_file_paths),
- ),
- (
- ContextFormatStyle.STUB,
- make_imports_pathspec_for_paths(relevant_file_paths, full_repo_contents),
- ),
- (
- ContextFormatStyle.FULL_FILE,
- make_docs_pathspec_along_paths(relevant_file_paths),
- ),
- (ContextFormatStyle.HIDDEN, FULL_REPO_PATHSPEC),
- ),
- )
- case SubrepoContextStrategyType.RELEVANT_WHOLE_FILES_DOCS as s:
- return SubrepoContextStrategy(
- label=s,
- matchers=(
- (
- ContextFormatStyle.FULL_FILE,
- make_relevant_files_pathspec(relevant_file_paths),
- ),
- (
- ContextFormatStyle.FULL_FILE,
- make_docs_pathspec_along_paths(relevant_file_paths),
- ),
- (ContextFormatStyle.HIDDEN, FULL_REPO_PATHSPEC),
- ),
- )
- case SubrepoContextStrategyType.RELEVANT_WHOLE_FILES_INSTRUCTIONS as s:
- return SubrepoContextStrategy(
- label=s,
- matchers=(
- (
- ContextFormatStyle.FULL_FILE,
- make_relevant_files_pathspec(relevant_file_paths),
- ),
- (
- ContextFormatStyle.FULL_FILE,
- make_instructions_pathspec_along_paths(relevant_file_paths),
- ),
- (ContextFormatStyle.HIDDEN, FULL_REPO_PATHSPEC),
- ),
- )
- case SubrepoContextStrategyType.RELEVANT_WHOLE_FILES as s:
- return SubrepoContextStrategy(
- label=s,
- matchers=(
- (
- ContextFormatStyle.FULL_FILE,
- make_relevant_files_pathspec(relevant_file_paths),
- ),
- (ContextFormatStyle.HIDDEN, FULL_REPO_PATHSPEC),
- ),
- )
- case SubrepoContextStrategyType.RELEVANT_STUBBIFIED_FILES as s:
- return SubrepoContextStrategy(
- label=s,
- matchers=(
- (
- ContextFormatStyle.STUB,
- make_relevant_files_pathspec(relevant_file_paths),
- ),
- (ContextFormatStyle.HIDDEN, FULL_REPO_PATHSPEC),
- ),
- )
- case SubrepoContextStrategyType.NOTHING as s:
- return SubrepoContextStrategy(
- label=s,
- matchers=((ContextFormatStyle.HIDDEN, FULL_REPO_PATHSPEC),),
- )
-
- case SubrepoContextStrategyType.WHOLE_DOCS_AND_OTHERWISE_FILENAMES as s:
- return SubrepoContextStrategy(
- label=s,
- matchers=(
- (ContextFormatStyle.FULL_FILE, DOC_PATHSPEC),
- (ContextFormatStyle.FILENAME_ONLY, FULL_REPO_PATHSPEC),
- ),
- )
- case SubrepoContextStrategyType.WHOLE_INSTRUCTIONS_AND_OTHERWISE_FILENAMES as s:
- return SubrepoContextStrategy(
- label=s,
- matchers=(
- (ContextFormatStyle.FULL_FILE, INSTRUCTIONS_PATHSPEC),
- (ContextFormatStyle.FILENAME_ONLY, FULL_REPO_PATHSPEC),
- ),
- )
- case SubrepoContextStrategyType.WHOLE_INSTRUCTIONS as s:
- return SubrepoContextStrategy(
- label=s,
- matchers=(
- (ContextFormatStyle.FULL_FILE, INSTRUCTIONS_PATHSPEC),
- (ContextFormatStyle.HIDDEN, FULL_REPO_PATHSPEC),
- ),
- )
-
- case SubrepoContextStrategyType.WHOLE_DOCS as s:
- return SubrepoContextStrategy(
- label=s,
- matchers=(
- (ContextFormatStyle.FULL_FILE, DOC_PATHSPEC),
- (ContextFormatStyle.HIDDEN, FULL_REPO_PATHSPEC),
- ),
- )
- case SubrepoContextStrategyType.WHOLE_INSTRUCTIONS_AND_RELEVANT_DOCS as s:
- return SubrepoContextStrategy(
- label=s,
- matchers=(
- (ContextFormatStyle.FULL_FILE, INSTRUCTIONS_PATHSPEC),
- (
- ContextFormatStyle.FULL_FILE,
- make_docs_pathspec_along_paths(relevant_file_paths),
- ),
- (ContextFormatStyle.HIDDEN, FULL_REPO_PATHSPEC),
- ),
- )
- case SubrepoContextStrategyType.RELEVANT_DOCS as s:
- return SubrepoContextStrategy(
- label=s,
- matchers=(
- (
- ContextFormatStyle.FULL_FILE,
- make_docs_pathspec_along_paths(relevant_file_paths),
- ),
- (ContextFormatStyle.HIDDEN, FULL_REPO_PATHSPEC),
- ),
- )
- case SubrepoContextStrategyType.RELEVANT_INSTRUCTIONS as s:
- return SubrepoContextStrategy(
- label=s,
- matchers=(
- (
- ContextFormatStyle.FULL_FILE,
- make_instructions_pathspec_along_paths(relevant_file_paths),
- ),
- (ContextFormatStyle.HIDDEN, FULL_REPO_PATHSPEC),
- ),
- )
- case _ as unreachable:
- assert_never(unreachable) # pyre-ignore[6]: pyre doesn't understand enums
-
-
-def generate_subrepo_strategies(
- mode: StrategyMode,
- full_repo_contents: InMemoryFileSystem,
- relevant_file_paths: frozenset[str] | None = None,
-) -> list[SubrepoContextStrategy]:
- available_info = AvailableInfoMode.YES_FILES if relevant_file_paths else AvailableInfoMode.NO_FILES
- return [
- build_strategy(strategy_type, full_repo_contents, relevant_file_paths)
- for strategy_type in DEFAULT_STRATEGY_TYPES[(mode, available_info)]
- ]
-
-
-def select_desired_subrepo_strategies(
- full_repo_contents: InMemoryFileSystem,
- relevant_file_paths: frozenset[str] | None = None,
- subrepo_context_config: str | None = None,
- strategy_types_to_try: tuple[SubrepoContextStrategyType] | None = None,
- strategy_mode: StrategyMode | None = None, # if no config option is set, defaults to StrategyMode.REGULAR
-) -> list[SubrepoContextStrategy]:
- num_ways_config_was_set = sum(
- 1 for v in [subrepo_context_config, strategy_types_to_try, strategy_mode] if v is not None
- )
- if num_ways_config_was_set > 1:
- assert False, "Can only specify one of subrepo_context_config, strategy_types_to_try, and strategy_mode"
-
- if subrepo_context_config is not None:
- # An explicit subrepo context config was provided. Use it exclusively.
- subrepo_context_matchers = parse_subrepo_context_matchers_from_toml(subrepo_context_config)
- return [
- SubrepoContextStrategy(
- label=SubrepoContextStrategyType.CUSTOM,
- matchers=subrepo_context_matchers,
- )
- ]
- elif strategy_types_to_try is not None:
- return [
- build_strategy(strategy_type, full_repo_contents, relevant_file_paths)
- for strategy_type in strategy_types_to_try
- ]
- else:
- strategy_mode_to_use = strategy_mode if strategy_mode is not None else StrategyMode.REGULAR
- return generate_subrepo_strategies(
- strategy_mode_to_use,
- full_repo_contents=full_repo_contents,
- relevant_file_paths=relevant_file_paths,
- )
-
-
-# Caching results because this function is quite expensive. We compose multiple repo_context prefixes, and
-# also have to tokenize them to check their respective lengths. Both of these operations are expensive,
-@functools.lru_cache(maxsize=10)
-def get_repo_context(
- model_config: LanguageModelGenerationConfig,
- full_repo_contents: InMemoryFileSystem,
- # how many tokens to reserve for additional prompt messages and output
- tokens_to_reserve: int,
- relevant_file_paths: frozenset[str] | None = None,
- subrepo_context_config: str | None = None,
- strategy_types_to_try: tuple[SubrepoContextStrategyType] | None = None,
- strategy_mode: StrategyMode | None = None, # if no config option is set, defaults to StrategyMode.REGULAR
- template: str = REPO_CONTEXT_TEMPLATE,
-) -> SubrepoContextWithFormattedContext:
- """
- Make sure to try pass the same `full_repo_contents` when making multiple similar calls.
- Ordering of the dict is relevant for caching.
- """
- subrepo_context_strategies_to_try = select_desired_subrepo_strategies(
- full_repo_contents,
- relevant_file_paths,
- subrepo_context_config,
- strategy_types_to_try,
- strategy_mode,
- )
-
- last_context_length_exceeded_error: ContextLengthExceededError | None = None
- for subrepo_context_strategy in subrepo_context_strategies_to_try:
- try:
- path_to_format_style = compute_file_context_format_styles(
- file_paths=full_repo_contents.text_files.keys(),
- subrepo_context_matchers=subrepo_context_strategy.matchers,
- exclusions=EXCLUSIONS_PATHSPEC,
- )
- repo_context_str, repo_context_files = format_subrepo_context(
- full_repo_contents=full_repo_contents.text_files,
- model_config=model_config,
- path_to_format_style=path_to_format_style,
- tokens_to_reserve=tokens_to_reserve,
- template=template,
- )
- logger.info("Selected subrepo context strategy: {}", subrepo_context_strategy.label)
-
- if subrepo_context_strategy.label == SubrepoContextStrategyType.NOTHING:
- # log an error if we have to use the NOTHING strategy, but still proceed with the call
- logger.error("Selected NOTHING subrepo context strategy; hopefully this doesn't happen too often!")
-
- return SubrepoContextWithFormattedContext(
- formatted_repo_context=repo_context_str,
- repo_context_files=repo_context_files,
- subrepo_context_strategy_label=subrepo_context_strategy.label,
- )
- except ContextLengthExceededError as e:
- last_context_length_exceeded_error = e
-
- # We have exhausted all subrepo context strategies, and none of them worked.
- assert last_context_length_exceeded_error is not None
- raise last_context_length_exceeded_error from last_context_length_exceeded_error
-
-
-# TODO: why not just render this here?
-def create_context_prompt_prefix(repo_context: str) -> tuple[str, Mapping[str, Any]]:
- """Create a message that provides context about the repo contents."""
- cached_prefix_template = """[ROLE=SYSTEM_CACHED]
-You are a detail-oriented, expert software developer.
-
-Your goal is to help the user develop a particular commit to make a change to their program.
-
-{{repo_context}}
-
-{% if recent_git_history -%}
-
-As additional context, here are some of the most recent changes made to the codebase (the output of `git log` and diffs for each of those commits):
-
-```
-{{recent_git_history}}
-```
-{% endif -%}
-"""
-
- return (
- cached_prefix_template,
- dict(
- repo_context=escape_prompt_markers(repo_context),
- recent_git_history=None,
- ),
- )
diff --git a/imbue_tools/imbue_tools/repo_utils/context_retrieval.py b/imbue_tools/imbue_tools/repo_utils/context_retrieval.py
@@ -1,121 +0,0 @@
-import threading
-import time
-from pathlib import Path
-from typing import Generator
-
-import pygit2
-from loguru import logger
-from pygit2.enums import ObjectType
-from pygit2.repository import Repository
-
-from imbue_core.async_utils import make_async
-from imbue_tools.repo_utils.diff_utils import apply_diffs_to_files
-from imbue_tools.repo_utils.file_system import FileContents
-from imbue_tools.repo_utils.file_system import InMemoryFileSystem
-from imbue_tools.repo_utils.file_system import SymlinkContents
-
-
-class RepoContextManagerError(Exception):
- pass
-
-
-class RepoContextManager:
- """A manager for handling retrieval of files, etc from the repo."""
-
- def __init__(self, repo_path: Path, project_name: str) -> None:
- self.project_name = project_name
- self.repo_path = repo_path
- self._repo = Repository(path=str(repo_path))
-
- # We need the sync lock due to pygit2 being synchronous.
- # It is mostly used for the blob data cache, but also for the repo contents by git hash cache.
- self._lock = threading.Lock()
-
- @classmethod
- def build(cls, repo_path: Path) -> "RepoContextManager":
- try:
- # make sure we are in a git repo
- Repository(path=str(repo_path))
- except pygit2.GitError as e:
- raise RepoContextManagerError(f"Failed to initialize git repo at {repo_path}") from e
-
- repo_context_manager = cls(repo_path=repo_path, project_name=repo_path.name)
- return repo_context_manager
-
- async def get_full_repo_contents_at_repo_state(self, git_hash: str, diff: str) -> InMemoryFileSystem:
- final_contents = await self.get_full_repo_contents_at_commit(git_hash)
- final_contents = await apply_diffs_to_files(final_contents, (diff,))
- return final_contents
-
- def get_full_repo_contents_at_commit_sync(self, git_hash: str) -> InMemoryFileSystem:
- # NOTE: most of the time we want to get the contents at a repo state, not a git hash.
- # Call get_full_repo_contents_at_repo_state instead in that case.
- with self._lock:
- start_time = time.perf_counter()
-
- # Assert against use of HEAD specifically because there could be some existing code
- # that uses it, and we want to catch that. It would fail below as well with a KeyError,
- # but this assert makes the exception message more explicit.
- assert git_hash != "HEAD", "Only proper commit hashes are supported, not HEAD"
- commit = self._repo[git_hash]
- assert isinstance(commit, pygit2.Commit), f"Expected a pygit2.Commit, got {type(commit)}"
-
- full_repo_contents = self._read_blobs_from_commit(commit)
-
- end_time = time.perf_counter()
- logger.debug(
- "Loaded full repo contents for git hash {git_hash} in {duration:.2f} seconds",
- git_hash=git_hash,
- duration=end_time - start_time,
- )
- return full_repo_contents
-
- @make_async
- def get_full_repo_contents_at_commit(self, git_hash: str) -> InMemoryFileSystem:
- return self.get_full_repo_contents_at_commit_sync(git_hash)
-
- def _read_blobs_from_commit(self, commit: pygit2.Commit) -> InMemoryFileSystem:
- """Read all blobs in a given commit."""
- file_system_dict: dict[str, FileContents] = {}
-
- for path, blob in self._list_blobs_from_tree(commit.tree, skip_binary=False, skip_symlinks=False):
- if blob.filemode == 0o120000:
- # Blob is a symbolic link. Its contents in git represent the target path.
- file_system_dict[path] = SymlinkContents(target_path=blob.data.decode("utf-8"))
- else:
- file_system_dict[path] = blob.data
- return InMemoryFileSystem.build(file_system_dict)
-
- def _list_blobs_from_tree(
- self, tree: pygit2.Tree, skip_binary: bool, skip_symlinks: bool
- ) -> Generator[tuple[str, pygit2.Blob], None, None]:
- """Recursively list all blobs in a tree, including its subtrees."""
- assert self._lock.locked()
- for entry in tree:
- if entry.type == ObjectType.BLOB:
- assert isinstance(entry, pygit2.Blob)
- if skip_binary and entry.is_binary:
- continue
- if skip_symlinks and entry.filemode == 0o120000:
- continue
-
- blob_path = entry.name
- assert blob_path is not None
- yield blob_path, entry
-
- elif entry.type == ObjectType.TREE:
- assert isinstance(entry, pygit2.Tree)
- # Recurse into a subtree (folder)
- sub_tree = self._repo[entry.id]
- assert isinstance(sub_tree, pygit2.Tree)
- for sub_path, sub_blob in self._list_blobs_from_tree(
- sub_tree, skip_binary=skip_binary, skip_symlinks=skip_symlinks
- ):
- yield f"{entry.name}/{sub_path}", sub_blob
-
- elif entry.type == ObjectType.COMMIT:
- # A COMMIT object indicates a submodule, which we do not traverse for the time being.
- logger.info("Skipping submodule in repo context: {}", entry.name)
-
- else:
- raise ValueError(f"Unexpected entry type in git tree: {entry.type}")
diff --git a/imbue_tools/imbue_tools/repo_utils/context_utils.py b/imbue_tools/imbue_tools/repo_utils/context_utils.py
@@ -1,52 +0,0 @@
-from pathlib import Path
-from typing import Iterable
-
-from imbue_tools.repo_utils.python_imports import QualifiedName
-
-
-def escape_prompt_markers(text: str) -> str:
- markers = [
- "[ROLE=ASSISTANT]",
- "[ROLE=USER]",
- "[ROLE=USER_CACHED]",
- "[ROLE=SYSTEM]",
- "[ROLE=SYSTEM_CACHED]",
- "[ROLE=HUMAN]",
- ]
- for marker in markers:
- text = text.replace(marker, f"[{marker}]")
- return text
-
-
-def escape_all_jinja_variables(text: str) -> str:
- return "{% raw %}" + text + "{% endraw %}"
-
-
-def does_relative_path_match_target_path_suffix(target_path: Path, relative_file_path: Path) -> bool:
- """
- Checks if the parts of a relative path match the suffix of a target path.
- """
- possible_parts = relative_file_path.parts
- target_parts = target_path.parts
-
- if len(possible_parts) > len(target_parts):
- return False
-
- for i in range(1, len(possible_parts) + 1):
- if possible_parts[-i] != target_parts[-i]:
- return False
- return True
-
-
-def maybe_get_file_path_from_qualified_name(
- qualified_name: QualifiedName, all_file_paths: Iterable[Path]
-) -> Path | None:
- """
- Tries to find the file path that corresponds to qualified name. This requires the qualified name to be a file in the repo.
- """
- possible_relative_file_path = qualified_name.to_path()
- # NOTE: it's possible to make this faster by doing some upfront computation
- for target_file_path in all_file_paths:
- if does_relative_path_match_target_path_suffix(target_file_path, possible_relative_file_path):
- return target_file_path
- return None
diff --git a/imbue_tools/imbue_tools/repo_utils/data_types.py b/imbue_tools/imbue_tools/repo_utils/data_types.py
@@ -1,51 +0,0 @@
-from abc import ABC
-from abc import abstractmethod
-from typing import Annotated
-
-from pydantic import Tag
-
-from imbue_core.pydantic_serialization import SerializableModel
-from imbue_core.pydantic_serialization import build_discriminator
-
-
-class FileContext(ABC, SerializableModel):
- object_type: str
- path: str
-
- @abstractmethod
- def format_for_agent(self) -> str:
- pass
-
-
-class FullFileContext(FileContext):
- object_type: str = "FullFileContext"
- path: str
- contents: str = "RAW FILE CONTENTS"
-
- def format_for_agent(self) -> str:
- return f"<FILE>\n<PATH>\n{self.path}\n</PATH>\n<CONTENTS>\n{self.contents}\n</CONTENTS>\n</FILE>\n\n"
-
-
-class FilenameContext(FileContext):
- object_type: str = "FilenameContext"
- path: str
-
- def format_for_agent(self) -> str:
- return f"<FILE>\n<PATH>\n{self.path}\n</PATH>\n</FILE>\n\n"
-
-
-class StubFileContext(FileContext):
- object_type: str = "StubFileContext"
- path: str
- stub: str
-
- def format_for_agent(self) -> str:
- return f"<FILE>\n<PATH>\n{self.path}\n</PATH>\n<STUBIFIED_CONTENTS>\n{self.stub}\n</STUBIFIED_CONTENTS>\n</FILE>\n\n"
-
-
-FileContextUnion = Annotated[
- Annotated[FullFileContext, Tag("FullFileContext")]
- | Annotated[FilenameContext, Tag("FilenameContext")]
- | Annotated[StubFileContext, Tag("StubFileContext")],
- build_discriminator(),
-]
diff --git a/imbue_tools/imbue_tools/repo_utils/diff_utils.py b/imbue_tools/imbue_tools/repo_utils/diff_utils.py
@@ -1,96 +0,0 @@
-import re
-import subprocess
-import tempfile
-from pathlib import Path
-
-import pygit2
-from async_lru import alru_cache # type: ignore[undefined-attribute]: pyre on modal has an issue with this
-from loguru import logger
-
-from imbue_tools.repo_utils.errors import DiffApplicationError
-from imbue_tools.repo_utils.file_system import FileContents
-from imbue_tools.repo_utils.file_system import InMemoryFileSystem
-from imbue_tools.repo_utils.file_system import SymlinkContents
-from imbue_tools.repo_utils.file_system_utils import (
- create_initial_placeholder_commit_for_dir,
-)
-from imbue_tools.repo_utils.file_system_utils import (
- temporary_local_dir_from_in_memory_file_system,
-)
-
-
-@alru_cache
-async def apply_diffs_to_files(file_contents: InMemoryFileSystem, diff_strings: tuple[str, ...]) -> InMemoryFileSystem:
- # Have to do this wrapping and unwrapping into dicts to allow @alru_cache to work
- files_with_diffs = file_contents
- for diff_string in diff_strings:
- files_with_diffs = await _apply_diff_to_files(file_contents=files_with_diffs, diff_string=diff_string)
- return files_with_diffs
-
-
-async def _apply_diff_to_files(file_contents: InMemoryFileSystem, diff_string: str) -> InMemoryFileSystem:
- if diff_string.strip() == "":
- return file_contents
-
- file_pattern = re.compile(r"^diff --git a/(.+?) b/(.+)$", re.MULTILINE)
- matches = file_pattern.findall(diff_string)
-
- relevant_file_contents_dict = {}
- for match in matches:
- assert len(match) == 2
- for file_path in match:
- contents = file_contents.get(file_path, None)
- if contents is not None:
- relevant_file_contents_dict[file_path] = contents
-
- async with temporary_local_dir_from_in_memory_file_system(
- InMemoryFileSystem.build(relevant_file_contents_dict)
- ) as temp_repo_dir:
- repo = pygit2.init_repository(temp_repo_dir, bare=False)
- create_initial_placeholder_commit_for_dir(repo)
-
- with tempfile.NamedTemporaryFile(delete=False) as temp_patch_file:
- temp_patch_file.write(diff_string.encode("utf-8"))
- temp_patch_file.flush()
- patch_file_path = temp_patch_file.name
-
- try:
- result = subprocess.run(
- ("git", "apply", "--verbose", patch_file_path),
- cwd=temp_repo_dir,
- capture_output=True,
- text=True,
- timeout=10.0,
- check=True,
- )
- except Exception as e:
- logger.trace("Unable to apply patch: {error}", error=e)
- raise DiffApplicationError from e
-
- try:
- updated_file_contents = _read_file_contents_from_dir_without_git(temp_repo_dir)
- except Exception as e:
- raise DiffApplicationError from e
-
- combined_file_contents_dict = dict(updated_file_contents.files)
- for file_path, contents in file_contents.files.items():
- if file_path not in relevant_file_contents_dict:
- combined_file_contents_dict[file_path] = contents
-
- return InMemoryFileSystem.build(combined_file_contents_dict)
-
-
-def _read_file_contents_from_dir_without_git(dir_path_str: str) -> InMemoryFileSystem:
- file_system_dict: dict[str, FileContents] = {}
- for file_path in Path(dir_path_str).rglob("*"):
- if ".git" in file_path.parts:
- continue
- if file_path.is_symlink():
- relative_path = str(file_path.relative_to(dir_path_str))
- target_path = str(file_path.readlink())
- file_system_dict[relative_path] = SymlinkContents(target_path=target_path)
- elif file_path.is_file():
- relative_path = str(file_path.relative_to(dir_path_str))
- with open(file_path, "rb") as file:
- file_system_dict[relative_path] = file.read()
- return InMemoryFileSystem.build(file_system_dict)
diff --git a/imbue_tools/imbue_tools/repo_utils/file_system.py b/imbue_tools/imbue_tools/repo_utils/file_system.py
@@ -1,71 +0,0 @@
-from typing import Generator
-from typing import Mapping
-
-from imbue_core.frozen_utils import FrozenDict
-from imbue_core.frozen_utils import deep_freeze_mapping
-from imbue_core.pydantic_serialization import SerializableModel
-
-
-class SymlinkContents(SerializableModel):
- """A special type to represent a symbolic link in a file system."""
-
- target_path: str
-
- # Need to make SymlinkContents non-Iterable, or else deep_freeze_mapping will convert this to a tuple in InMemoryFileSystem.build.
- __iter__ = None # type: ignore
-
-
-DecodedTextFileContents = str
-FileContents = bytes | SymlinkContents
-
-
-class InMemoryFileSystem(SerializableModel):
- """
- A simple representation of in-memory file system. Can contain both text and binary files.
- """
-
- # Mapping from file path to contents
- files: FrozenDict[str, FileContents]
- # Only text files, decoded as UTF-8. Excludes symlinks and binary files.
- text_files: FrozenDict[str, DecodedTextFileContents]
-
- @classmethod
- def build(cls, files: Mapping[str, FileContents]) -> "InMemoryFileSystem":
- sorted_files = {k: v for k, v in sorted(files.items())}
- sorted_decoded_files: dict[str, DecodedTextFileContents | None] = {
- k: _try_decode_file_contents(c) for k, c in sorted_files.items()
- }
- sorted_text_files: dict[str, DecodedTextFileContents] = {
- k: c for k, c in sorted_decoded_files.items() if c is not None
- }
- return cls(
- files=deep_freeze_mapping(sorted_files),
- text_files=deep_freeze_mapping(sorted_text_files),
- )
-
- def get(self, file_path: str, default: FileContents | None = None) -> FileContents | None:
- if file_path in self.files:
- return self.files[file_path]
- return default
-
- def get_text(
- self, file_path: str, default: DecodedTextFileContents | None = None
- ) -> DecodedTextFileContents | None:
- """Get a the contents of a text file as a string. Returns `default` if the file does not exist, is a symlink, or is a binary file."""
- if file_path in self.text_files:
- return self.text_files[file_path]
- return default
-
- def __iter__(self) -> Generator[tuple[str, FileContents], None, None]:
- return (file for file in self.files.items())
-
-
-def _try_decode_file_contents(contents: FileContents) -> DecodedTextFileContents | None:
- if isinstance(contents, SymlinkContents):
- return None
- else:
- assert isinstance(contents, bytes)
- try:
- return contents.decode("utf-8")
- except UnicodeDecodeError:
- return None
diff --git a/imbue_tools/imbue_tools/repo_utils/file_system_utils.py b/imbue_tools/imbue_tools/repo_utils/file_system_utils.py
@@ -1,70 +0,0 @@
-import asyncio
-import tempfile
-from contextlib import asynccontextmanager
-from pathlib import Path
-from typing import AsyncGenerator
-from typing import cast
-
-import anyio
-import pygit2
-from loguru import logger
-
-from imbue_tools.repo_utils.file_system import FileContents
-from imbue_tools.repo_utils.file_system import InMemoryFileSystem
-from imbue_tools.repo_utils.file_system import SymlinkContents
-
-
-async def write_file_contents_to_dir(file_contents: InMemoryFileSystem, dir_path_str: str) -> None:
- dir_path = Path(dir_path_str)
- tasks = [
- asyncio.create_task(_write_single_file_to_dir(dir_path / file_path, content))
- for file_path, content in file_contents.files.items()
- ]
- await asyncio.gather(*tasks)
-
-
-async def _write_single_file_to_dir(full_path: Path, content: FileContents) -> None:
- await anyio.to_thread.run_sync(_write_file_sync, full_path, content)
-
-
-def _write_file_sync(full_path: Path, content: FileContents) -> None:
- full_path.parent.mkdir(parents=True, exist_ok=True)
- if isinstance(content, bytes):
- full_path.write_bytes(content)
- elif isinstance(content, SymlinkContents):
- full_path.symlink_to(content.target_path)
- else:
- logger.error(
- "Tried to write contents that were neither bytes nor SymlinkContents: {content}",
- content=content,
- )
-
-
-@asynccontextmanager
-async def temporary_local_dir_from_in_memory_file_system(
- file_contents: InMemoryFileSystem,
-) -> AsyncGenerator[str, None]:
- with tempfile.TemporaryDirectory() as temp_dir:
- await write_file_contents_to_dir(file_contents, temp_dir)
- yield temp_dir
-
-
-def create_initial_placeholder_commit_for_dir(repo: pygit2.Repository) -> pygit2.Commit:
- # pyre-ignore[16]: pyre doesn't understand the inheritance of Repository from BaseRepository
- repo_index = repo.index
- repo_index.add_all()
- repo_index.write()
- tree = repo_index.write_tree()
- signature = pygit2.Signature("placeholder", "placeholder@example.com")
-
- commit_oid = repo.create_commit(
- "refs/heads/master",
- signature,
- signature,
- "placeholder commit for diff utils",
- tree,
- [],
- )
- # pyre-ignore[16]: pyre doesn't understand the inheritance of Repository from BaseRepository
- commit = repo.get(commit_oid)
- return cast(pygit2.Commit, commit)
diff --git a/imbue_tools/imbue_tools/repo_utils/project_context.py b/imbue_tools/imbue_tools/repo_utils/project_context.py
@@ -1,234 +0,0 @@
-from functools import cached_property
-from functools import lru_cache
-from pathlib import Path
-from typing import Annotated
-from typing import Self
-
-import jinja2
-from pydantic import Tag
-from pydantic import computed_field
-
-from imbue_core.agents.configs import LanguageModelGenerationConfig
-from imbue_core.agents.configs import OpenAICompatibleModelConfig
-from imbue_core.async_utils import sync
-from imbue_core.frozen_utils import FrozenDict
-from imbue_core.pydantic_serialization import SerializableModel
-from imbue_core.pydantic_serialization import build_discriminator
-from imbue_tools.repo_utils.context_prefix import StrategyMode
-from imbue_tools.repo_utils.context_prefix import SubrepoContext
-from imbue_tools.repo_utils.context_prefix import SubrepoContextWithFormattedContext
-from imbue_tools.repo_utils.context_prefix import create_context_prompt_prefix
-from imbue_tools.repo_utils.context_prefix import get_repo_context
-from imbue_tools.repo_utils.context_retrieval import RepoContextManager
-from imbue_tools.repo_utils.file_system import InMemoryFileSystem
-from imbue_tools.repo_utils.subrepo_formatting import (
- REPO_CONTEXT_TEMPLATE_WITH_NO_MENTION_OF_DIFF,
-)
-
-
-@lru_cache
-def _get_repo_context_manager_for_repo_path(repo_path: Path) -> RepoContextManager:
- """
- Wrapper around RepoContextManager.build() to cache the resulting repo context manager.
-
- Internally, the RepoContextManager object will itself cache the repo contents.
- """
- return RepoContextManager.build(repo_path)
-
-
-class BaseProjectContext(SerializableModel):
- """
- Holds the context of the checked project including all its files.
-
- For LLM-based issue identifiers, no matter the scope, we want to use a fixed prompt prefix to leverage caching.
- We use the stable cached_prompt_prefix for that purpose.
-
- """
-
- object_type: str = "BaseProjectContext"
-
- file_contents_by_path: FrozenDict[str, str]
- cached_prompt_prefix: str
- # 0 - n most recent commits, with the most recent one being the first.
- # The state of the project (file_contents_by_path) is the state after the most recent commit.
-
- subrepo_context: SubrepoContext | None = None
- instruction_context: SubrepoContext | None = None
- repo_path: Path | None = None
-
- def get_file_contents(self, file_path: str) -> str | None:
- return self.file_contents_by_path.get(file_path)
-
- def get_computed_contexts(
- self,
- ) -> tuple[SubrepoContext | None, SubrepoContext | None]:
- """To match usage for LazyProjectContext; all fields are always computed because this isn't lazy"""
- return self.subrepo_context, self.instruction_context
-
-
-class LazyProjectContext(SerializableModel):
- object_type: str = "LazyProjectContext"
-
- base_commit: str
- diff: str
- language_model_name: str
- repo_path: Path
- tokens_to_reserve: int
-
- # Optional context window override. If not provided, the model's default context window
- # will be looked up from the model registry (which fails for unknown models).
- context_window: int | None = None
-
- # If True, this is a custom/user-defined model (uses approximate token counting).
- is_custom_model: bool = False
-
- def get_file_contents(self, file_path: str) -> str | None:
- return self.file_contents_by_path.get(file_path)
-
- @classmethod
- def build(
- cls,
- base_commit: str,
- diff: str,
- language_model_name: str,
- repo_path: Path,
- # How many tokens to keep for the vet specific prompt and any output tokens.
- tokens_to_reserve: int,
- context_window: int | None = None,
- is_custom_model: bool = False,
- ) -> Self:
- return cls(
- base_commit=base_commit,
- diff=diff,
- language_model_name=language_model_name,
- repo_path=repo_path,
- tokens_to_reserve=tokens_to_reserve,
- context_window=context_window,
- is_custom_model=is_custom_model,
- )
-
- # The fields are computed and cached because they are quite expensive to compute.
- # We compose multiple repo_context prefixes, and also have to tokenize them to check their respective lengths.
- # Both of these operations are expensive
-
- @computed_field
- @cached_property
- def repo_context_manager(self) -> RepoContextManager:
- return _get_repo_context_manager_for_repo_path(self.repo_path)
-
- @computed_field
- @cached_property
- def original_content_by_path(self) -> InMemoryFileSystem:
- original_content_by_path = sync(self.repo_context_manager.get_full_repo_contents_at_commit)(self.base_commit)
- return original_content_by_path
-
- @computed_field
- @cached_property
- def content_by_path(self) -> InMemoryFileSystem:
- if self.diff:
- return sync(self.repo_context_manager.get_full_repo_contents_at_repo_state)(self.base_commit, self.diff)
- else:
- return self.original_content_by_path
-
- @computed_field
- @property
- def file_contents_by_path(self) -> FrozenDict[str, str]:
- return self.content_by_path.text_files
-
- @computed_field
- @cached_property
- def cached_prompt_prefix(self) -> str:
- prompt_prefix_template, prompt_prefix_params = create_context_prompt_prefix(
- repo_context=self.subrepo_context.formatted_repo_context,
- )
- env = jinja2.Environment(undefined=jinja2.StrictUndefined)
- jinja_template = env.from_string(prompt_prefix_template)
- cached_prompt_prefix = jinja_template.render(**prompt_prefix_params)
- return cached_prompt_prefix
-
- @computed_field
- @cached_property
- def modified_file_paths(self) -> frozenset[str]:
- modified_file_paths = []
- for file_path in self.content_by_path.files.keys():
- if self.content_by_path.get(file_path) != self.original_content_by_path.get(file_path):
- modified_file_paths.append(file_path)
- return frozenset(modified_file_paths)
-
- def _create_model_config(self) -> LanguageModelGenerationConfig:
- """Create the appropriate model config for context building.
-
- For custom models (is_custom_model=True), creates an OpenAICompatibleModelConfig
- that uses approximate token counting and the specified context window.
-
- For known models, creates a standard LanguageModelGenerationConfig that uses
- the model registry for token counting and context window lookup.
- """
- if self.is_custom_model:
- if self.context_window is None:
- raise ValueError(
- "context_window must be provided when is_custom_model=True "
- + "(custom models don't have a known context window)"
- )
- return OpenAICompatibleModelConfig(
- model_name=self.language_model_name,
- custom_base_url="", # Not used for context building
- custom_api_key_env="", # Not used for context building
- custom_context_window=self.context_window,
- custom_max_output_tokens=0, # Not used for context building
- )
- else:
- return LanguageModelGenerationConfig(model_name=self.language_model_name)
-
- @computed_field
- @cached_property
- def subrepo_context(self) -> SubrepoContextWithFormattedContext:
- model_config = self._create_model_config()
-
- subrepo_context = get_repo_context(
- full_repo_contents=self.content_by_path,
- model_config=model_config,
- relevant_file_paths=self.modified_file_paths,
- tokens_to_reserve=self.tokens_to_reserve,
- )
- return subrepo_context
-
- def to_base_project_context(self) -> BaseProjectContext:
- return BaseProjectContext(
- file_contents_by_path=self.file_contents_by_path,
- cached_prompt_prefix=self.cached_prompt_prefix,
- subrepo_context=self.subrepo_context,
- instruction_context=self.instruction_context,
- repo_path=self.repo_path,
- )
-
- @computed_field
- @cached_property
- def instruction_context(self) -> SubrepoContextWithFormattedContext:
- model_config = self._create_model_config()
- return get_repo_context(
- model_config=model_config,
- full_repo_contents=self.content_by_path,
- relevant_file_paths=None,
- tokens_to_reserve=self.tokens_to_reserve,
- strategy_mode=StrategyMode.DOCS,
- template=REPO_CONTEXT_TEMPLATE_WITH_NO_MENTION_OF_DIFF,
- )
-
- def get_computed_contexts(
- self,
- ) -> tuple[
- SubrepoContextWithFormattedContext | None,
- SubrepoContextWithFormattedContext | None,
- ]:
- """Returns subrepo context and instruction context, but only if they have already been computed; those that haven't been computed are None"""
- # checking for presence in __dict__ does not trigger computation
- subrepo_context = self.subrepo_context if "subrepo_context" in self.__dict__ else None
- instruction_context = self.instruction_context if "instruction_context" in self.__dict__ else None
- return subrepo_context, instruction_context
-
-
-ProjectContext = Annotated[
- Annotated[BaseProjectContext, Tag("BaseProjectContext")] | LazyProjectContext,
- build_discriminator(),
-]
diff --git a/imbue_tools/imbue_tools/repo_utils/python_imports.py b/imbue_tools/imbue_tools/repo_utils/python_imports.py
@@ -1,130 +0,0 @@
-import ast
-import sys
-from pathlib import Path
-
-from imbue_core.pydantic_serialization import SerializableModel
-
-STANDARD_LIBRARIES: frozenset[str] = sys.stdlib_module_names | frozenset(sys.builtin_module_names)
-
-
-class QualifiedName(SerializableModel):
- """A qualified name like 'foo.bar.baz'."""
-
- value: str
-
- @property
- def top_level_name(self) -> "QualifiedName":
- """Return the top-level module name (e.g., 'foo' from 'foo.bar.baz')."""
- return QualifiedName(value=self.value.split(".", maxsplit=1)[0])
-
- @property
- def parent_name(self) -> "QualifiedName":
- """Return the parent module name (e.g., 'foo.bar' from 'foo.bar.baz')."""
- return QualifiedName(value=self.value.rsplit(".", maxsplit=1)[0])
-
- def to_path(self) -> Path:
- """Convert qualified name to a file path (e.g., 'foo.bar' -> 'foo/bar.py')."""
- return Path(self.value.replace(".", "/") + ".py")
-
-
-class Import(SerializableModel):
- """Represents a single import statement."""
-
- source: str
- alias: str | None
- qualified_name: QualifiedName
-
-
-def _collect_global_imports(node: ast.AST, imports: list[Import]) -> None:
- """
- Recursively collect import statements at global scope.
-
- Stops recursing into function and class definitions since imports
- inside those are not at global scope.
-
- Args:
- node: The AST node to process
- imports: List to accumulate found imports
- """
- if isinstance(node, ast.Import):
- # Handle: import foo, bar
- # Handle: import foo as bar
- for alias in node.names:
- if alias.asname:
- source = f"import {alias.name} as {alias.asname}"
- alias_name = alias.asname
- else:
- source = f"import {alias.name}"
- alias_name = None
- imports.append(
- Import(
- source=source,
- alias=alias_name,
- qualified_name=QualifiedName(value=alias.name),
- )
- )
- elif isinstance(node, ast.ImportFrom):
- # Handle: from foo import bar, baz
- module = node.module or ""
- if node.names[0].name == "*":
- # from foo import *
- source = f"from {module} import *"
- imports.append(
- Import(
- source=source,
- alias=None,
- qualified_name=QualifiedName(value=f"{module}.*"),
- )
- )
- else:
- for alias in node.names:
- if module:
- full_name = f"{module}.{alias.name}"
- else:
- # relative import like: from . import foo
- full_name = alias.name
-
- if alias.asname:
- source = f"from {module} import {alias.name} as {alias.asname}"
- alias_name = alias.asname
- else:
- source = f"from {module} import {alias.name}"
- alias_name = None
-
- imports.append(
- Import(
- source=source,
- alias=alias_name,
- qualified_name=QualifiedName(value=full_name),
- )
- )
-
- # Don't recurse into function or class definitions - imports there are not global
- if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)):
- return
-
- # Recurse into child nodes
- for child in ast.iter_child_nodes(node):
- _collect_global_imports(child, imports)
-
-
-def get_global_imports(source_code: str) -> tuple[Import, ...]:
- """
- Extract all global imports from Python source code.
-
- This includes imports at module level, as well as imports inside conditionals
- or other control flow structures at the top level (not inside functions or classes).
-
- Args:
- source_code: Python source code as a string
-
- Returns:
- Tuple of Import objects representing all imports in the file
-
- Raises:
- SyntaxError: If the source code cannot be parsed
- """
- tree = ast.parse(source_code)
- imports: list[Import] = []
- _collect_global_imports(tree, imports)
- return tuple(imports)
diff --git a/imbue_tools/imbue_tools/repo_utils/subrepo_formatting.py b/imbue_tools/imbue_tools/repo_utils/subrepo_formatting.py
@@ -1,345 +0,0 @@
-import functools
-from enum import Enum
-from typing import Annotated
-from typing import Iterable
-from typing import Mapping
-from typing import Self
-from typing import assert_never
-
-import jinja2
-from pathspec import GitIgnoreSpec
-from pydantic import Tag
-
-from imbue_core.agents.configs import LanguageModelGenerationConfig
-from imbue_core.pydantic_serialization import SerializableModel
-from imbue_core.pydantic_serialization import build_discriminator
-from imbue_tools.repo_utils.context_utils import escape_all_jinja_variables
-from imbue_tools.repo_utils.context_utils import escape_prompt_markers
-from imbue_tools.repo_utils.data_types import FileContext
-from imbue_tools.repo_utils.data_types import FileContextUnion
-from imbue_tools.repo_utils.data_types import FilenameContext
-from imbue_tools.repo_utils.data_types import FullFileContext
-from imbue_tools.repo_utils.data_types import StubFileContext
-from imbue_tools.repo_utils.errors import ContextLengthExceededError
-from imbue_tools.repo_utils.stubify_file import stubify_code_file
-
-
-class ContextFormatStyle(Enum):
- FULL_FILE = "FULL_FILE"
- STUB = "STUB"
- FILENAME_ONLY = "FILENAME_ONLY"
- HIDDEN = "HIDDEN"
-
-
-class BaseFilenamePattern(SerializableModel):
- """
- Extends the functionality of `GitIgnoreSpec` to be serializable.
- """
-
- object_type: str = "BaseFilenamePattern"
- lines: tuple[str, ...]
-
- @functools.cached_property
- def git_ignore_spec(self) -> GitIgnoreSpec:
- return GitIgnoreSpec.from_lines(self.lines)
-
- @classmethod
- def from_lines(cls, lines: Iterable[str]) -> Self:
- return cls(lines=tuple(sorted(lines)))
-
- def match_file(self, file: str) -> bool:
- return self.git_ignore_spec.match_file(file)
-
-
-class NegatedFilenamePattern(BaseFilenamePattern):
- """Matches everything except the files matched by the base pattern."""
-
- object_type: str = "NegatedFilenamePattern"
-
- def match_file(self, file: str) -> bool:
- return not self.git_ignore_spec.match_file(file)
-
- @classmethod
- def build_from_positive_pattern(cls, positive_pattern: BaseFilenamePattern) -> Self:
- return cls(lines=positive_pattern.lines)
-
-
-class IntersectionFilenamePattern(SerializableModel):
- object_type: str = "IntersectionFilenamePattern"
- specs: tuple["FilenamePattern", ...]
-
- def match_file(self, file: str) -> bool:
- return all(spec.match_file(file) for spec in self.specs)
-
-
-class UnionFilenamePattern(SerializableModel):
- object_type: str = "UnionFilenamePattern"
- specs: tuple["FilenamePattern", ...]
-
- def match_file(self, file: str) -> bool:
- return any(spec.match_file(file) for spec in self.specs)
-
-
-class ExactFilenamePattern(SerializableModel):
- """
- Similar to a BaseFilenamePattern, but more efficient thanks to the use of a hash set.
- However, it only supports exact filename matches and no patterns.
-
- O(1) matching over n filenames, instead of O(n) with BaseFilenamePattern.
- """
-
- object_type: str = "ExactFilenamePattern"
- # Will match these exact filenames.
- # We store this as a tuple instead of frozenset to have deterministic ordering. Helps with snapshot tests.
- filenames: tuple[str, ...]
-
- def match_file(self, file: str) -> bool:
- return file in self.filenames_set
-
- @functools.cached_property
- def filenames_set(self) -> set[str]:
- return set(self.filenames)
-
-
-FilenamePattern = Annotated[
- Annotated[BaseFilenamePattern, Tag("BaseFilenamePattern")]
- | Annotated[NegatedFilenamePattern, Tag("NegatedFilenamePattern")]
- | Annotated[IntersectionFilenamePattern, Tag("IntersectionFilenamePattern")]
- | Annotated[UnionFilenamePattern, Tag("UnionFilenamePattern")]
- | Annotated[ExactFilenamePattern, Tag("ExactFilenamePattern")],
- build_discriminator(),
-]
-
-
-SubrepoContextMatchers = tuple[tuple[ContextFormatStyle, FilenamePattern], ...]
-
-
-@functools.lru_cache(maxsize=100)
-def stubify_file_contents_cached(path: str, contents: str) -> str:
- # TODO: there's various flags here we could try
- # TODO: we may want an option to suppress comments, which end up being a large percent of the lines
- if path.endswith(".py"):
- return stubify_code_file(path, contents, keep_indent=True)
- else:
- # For non-Python files, maintain the full contents for now.
- return contents
-
-
-def stubify_and_format_for_agent_context(path: str, contents: str | None) -> StubFileContext:
- contents_to_use = contents if contents is not None else "RAW FILE CONTENTS"
- stub = stubify_file_contents_cached(path=path, contents=contents_to_use)
- return StubFileContext(path=path, stub=stub)
-
-
-def format_filename_only_for_agent_context(path: str) -> FilenameContext:
- return FilenameContext(path=path)
-
-
-def format_full_file_for_agent_context(path: str, contents: str) -> FullFileContext:
- return FullFileContext(path=path, contents=contents)
-
-
-BASE_REPO_CONTEXT_TEMPLATE = """
-<REPO_CONTEXT>
-{{repo_context}}
-</REPO_CONTEXT>
-"""
-
-REPO_CONTEXT_TEMPLATE_WITH_NO_MENTION_OF_DIFF = (
- """
-{% if not is_shortened %}For context, here are the contents of all files currently in the project:
-{% else %}The project's repository is too large to show in full, so we have chosen a useful subset for your context.
-Files in the repository may be shown in either full, stubified, or filename-only form{% if has_hidden_files %}, or they may be hidden entirely{% endif %}. Here are the contents of some of the files in the repository:{% endif %}
-"""
- + BASE_REPO_CONTEXT_TEMPLATE
-)
-
-REPO_CONTEXT_TEMPLATE = (
- REPO_CONTEXT_TEMPLATE_WITH_NO_MENTION_OF_DIFF
- + """
-
-If any files have been changed, the changes will be described next. Otherwise, you can assume this is the current state of the project.
-"""
-)
-
-
-def format_file_for_agent_context(
- path: str, contents: str, format_style: ContextFormatStyle
-) -> FileContextUnion | None:
- if format_style == ContextFormatStyle.FULL_FILE:
- return format_full_file_for_agent_context(path, contents)
- elif format_style == ContextFormatStyle.STUB:
- return stubify_and_format_for_agent_context(path, contents)
- elif format_style == ContextFormatStyle.FILENAME_ONLY:
- return format_filename_only_for_agent_context(path)
- elif format_style == ContextFormatStyle.HIDDEN:
- return None
- else:
- assert_never(format_style) # pyre-ignore[6]: pyre doesn't understand enums
-
-
-@functools.lru_cache(maxsize=20)
-def parse_subrepo_context_matchers_from_toml(
- subrepo_context_config_toml: str,
-) -> SubrepoContextMatchers:
- current_mode: ContextFormatStyle
- matchers = []
- for line in subrepo_context_config_toml.splitlines():
- line = line.strip()
- if not line:
- continue
- if line.startswith("["):
- current_mode = ContextFormatStyle[line.replace("[", "").replace("]", "").upper()] # type: ignore
- continue
-
- exclude_spec = BaseFilenamePattern.from_lines([line])
- matchers.append((current_mode, exclude_spec))
- return tuple(matchers)
-
-
-def compute_file_format_style(
- file_path: str,
- subrepo_context_matchers: SubrepoContextMatchers,
- exclusions: FilenamePattern | None = None,
-) -> ContextFormatStyle:
- for matcher_format, matcher in subrepo_context_matchers:
- if matcher.match_file(file_path):
- if exclusions and matcher_format != ContextFormatStyle.HIDDEN and exclusions.match_file(file_path):
- # Exclusions override any non-hidden format and downgrade it to FILENAME_ONLY.
- return ContextFormatStyle.FILENAME_ONLY
- # Return the first match
- return matcher_format
- return ContextFormatStyle.HIDDEN
-
-
-def compute_file_context_format_styles(
- file_paths: Iterable[str],
- subrepo_context_matchers: SubrepoContextMatchers,
- exclusions: FilenamePattern | None = None,
-) -> Mapping[str, ContextFormatStyle]:
- return {
- file_path: compute_file_format_style(file_path, subrepo_context_matchers, exclusions)
- for file_path in file_paths
- }
-
-
-def get_estimated_lower_bound_token_count_for_text_and_model(text: str, model_name: str) -> int:
- # A factor of 1/4.5 appears to be a reasonable empirical estimate for current models.
- # We use a slighly smaller factor (1/5) to give more of a lower bound estimate.
- return round(len(text) / 5)
-
-
-def format_all_for_agent(repo_contents: tuple[FileContext, ...]) -> dict[str, str]:
- return {contents.path: contents.format_for_agent() for contents in repo_contents}
-
-
-def format_subrepo(formatted_repo_contents: Mapping[str, str]) -> str:
- repo_context_str = "".join([contents for contents in formatted_repo_contents.values() if contents is not None])
- return escape_all_jinja_variables(escape_prompt_markers(repo_context_str))
-
-
-def formatted_subrepo_to_prompt(
- repo_context_str: str, is_shortened: bool, has_hidden_files: bool, template: str
-) -> str:
- env = jinja2.Environment(undefined=jinja2.StrictUndefined)
- jinja_template = env.from_string(template)
- repo_context_prompt = jinja_template.render(
- repo_context=repo_context_str,
- is_shortened=is_shortened,
- has_hidden_files=has_hidden_files,
- )
- return repo_context_prompt
-
-
-def format_subrepo_context_into_filecontexts(
- full_repo_contents: Mapping[str, str],
- path_to_format_style: Mapping[str, ContextFormatStyle],
-) -> tuple[FileContextUnion, ...]:
- repo_contents = tuple(
- format_file_for_agent_context(path, contents, path_to_format_style[path])
- for path, contents in full_repo_contents.items()
- )
- repo_contents_with_hidden_removed = tuple(contents for contents in repo_contents if contents is not None)
- return repo_contents_with_hidden_removed
-
-
-def build_context_from_filecontexts(
- repo_contents_with_hidden_removed: tuple[FileContextUnion, ...],
- model_config: LanguageModelGenerationConfig,
- # how many tokens to reserve for additional prompt messages and output
- tokens_to_reserve: int,
- template: str = REPO_CONTEXT_TEMPLATE,
- path_to_format_style: Mapping[str, ContextFormatStyle] | None = None,
-) -> str:
- """
- Returns the repo contents formatted according to the format styles as a string.
- Includes (at least if the default template is used) an explanation at the beginning
- saying that these are the contents of the repo, potentially truncated or hidden.
-
- If there are no repo contents, returns an empty string.
- """
-
- if not repo_contents_with_hidden_removed:
- return ""
-
- max_context_length = model_config.get_max_context_length()
- available_tokens = max_context_length - tokens_to_reserve
-
- formatted_repo_contents_with_hidden_removed = format_all_for_agent(repo_contents_with_hidden_removed)
-
- repo_context_str = format_subrepo(formatted_repo_contents_with_hidden_removed)
-
- if model_config.is_custom_model():
- # For custom models, approximate_token_count is already fast, so skip the estimation step.
- full_repo_context_token_count = model_config.count_tokens(repo_context_str)
- else:
- # First use an estimation of the token count to see if we are likely below the maximum length. Then
- # double-check with the exact token count.
- # We do this because getting the exact token count is quite slow.
- estimated_full_repo_context_token_count = get_estimated_lower_bound_token_count_for_text_and_model(
- repo_context_str, model_config.model_name
- )
- if estimated_full_repo_context_token_count > available_tokens:
- raise ContextLengthExceededError(
- f"Estimated context has size {estimated_full_repo_context_token_count}; available tokens {available_tokens}"
- )
- full_repo_context_token_count = model_config.count_tokens(repo_context_str)
- if full_repo_context_token_count > available_tokens:
- raise ContextLengthExceededError(
- f"Context has size {full_repo_context_token_count}; available tokens {available_tokens}"
- )
-
- if path_to_format_style is None:
- path_to_format_style = {
- contents.path: ContextFormatStyle.FULL_FILE for contents in repo_contents_with_hidden_removed
- }
-
- is_shortened = any([style != ContextFormatStyle.FULL_FILE for style in path_to_format_style.values()])
- has_hidden_files = any([style == ContextFormatStyle.HIDDEN for style in path_to_format_style.values()])
-
- repo_context_prompt = formatted_subrepo_to_prompt(
- repo_context_str=repo_context_str,
- is_shortened=is_shortened,
- has_hidden_files=has_hidden_files,
- template=template,
- )
- return repo_context_prompt
-
-
-def format_subrepo_context(
- full_repo_contents: Mapping[str, str],
- path_to_format_style: Mapping[str, ContextFormatStyle],
- model_config: LanguageModelGenerationConfig,
- # how many tokens to reserve for additional prompt messages and output
- tokens_to_reserve: int,
- template: str = REPO_CONTEXT_TEMPLATE,
-) -> tuple[str, tuple[FileContextUnion, ...]]:
- repo_contents_tuple = format_subrepo_context_into_filecontexts(full_repo_contents, path_to_format_style)
- repo_context_str = build_context_from_filecontexts(
- repo_contents_tuple,
- model_config,
- tokens_to_reserve,
- template,
- path_to_format_style,
- )
- return repo_context_str, repo_contents_tuple
diff --git a/imbue_tools/imbue_tools/types/vet_config.py b/imbue_tools/imbue_tools/types/vet_config.py
@@ -1,100 +0,0 @@
-from pathlib import Path
-
-from imbue_core.agents.configs import LanguageModelGenerationConfig
-from imbue_core.agents.llm_apis.anthropic_api import AnthropicModelName
-from imbue_core.data_types import IssueCode
-from imbue_core.pydantic_serialization import SerializableModel
-
-DEFAULT_CONFIDENCE_THRESHOLD = 0.8
-
-
-class VetConfig(SerializableModel):
- """Configuration for the vet system."""
-
- # If none, all registered identifiers are used.
- # Otherwise, only the identifiers in this tuple are used.
- enabled_identifiers: tuple[str, ...] | None = None
-
- # Issue identifiers that are disabled are never used.
- disabled_identifiers: tuple[str, ...] | None = None
-
- # Similar to the above, but for reporting specific types of issues.
- # (Use the values from the vet.data_types.IssueCode enum.)
- enabled_issue_codes: tuple[IssueCode, ...] | None = None
- disabled_issue_codes: tuple[IssueCode, ...] | None = ()
-
- # Todo: Different models for different issue identifiers
- language_model_generation_config: LanguageModelGenerationConfig = LanguageModelGenerationConfig(
- model_name=AnthropicModelName.CLAUDE_4_5_HAIKU_2025_10_01
- )
- max_identifier_spend_dollars: float = 5.0
- max_output_tokens: int = 20000
- enable_parallel_agentic_issue_identification: bool = False
- max_identify_workers: int | None = None
- temperature: float = 0.5
-
- # If True, apply an additional LLM-based filtering stage, where each identified issue is evaluated
- # according to a number of quality criteria. Only issues that pass the evaluation are returned.
- filter_issues: bool = True
- filter_issues_through_llm_evaluator: bool = True
- filter_issues_below_confidence: float | None = DEFAULT_CONFIDENCE_THRESHOLD
-
- enable_deduplication: bool = True
- enable_collation: bool = True
-
- # If True, we attempt to cache the full prompts including specific inputs with the LLM provider.
- # There can be an additional cost for such a cache write, but it can help save cost in evaluation
- # contexts (such as black_box_evals) where the same inputs are being evaluated multiple times.
- cache_full_prompt: bool = False
-
- extra_context: str | None = None
-
- @classmethod
- def build(
- cls,
- language_model_name: str | None = None,
- language_model_cache_path: Path | None = None,
- enabled_identifiers: tuple[str, ...] | None = None,
- enable_parallel_agentic_issue_identification: bool = False,
- max_identify_workers: int | None = None,
- filter_issues: bool = True,
- filter_issues_below_confidence: float | None = DEFAULT_CONFIDENCE_THRESHOLD,
- enable_deduplication: bool = True,
- enable_collation: bool = True,
- enabled_issue_codes: tuple[IssueCode, ...] | None = None,
- temperature: float = 0.5,
- retry_jitter_factor: float = 0.0,
- cache_full_prompt: bool = False,
- ) -> "VetConfig":
- if not language_model_name:
- language_model_name = AnthropicModelName.CLAUDE_4_5_HAIKU_2025_10_01
- language_model_generation_config = LanguageModelGenerationConfig(
- model_name=language_model_name,
- cache_path=language_model_cache_path,
- retry_jitter_factor=retry_jitter_factor,
- )
- return cls(
- language_model_generation_config=language_model_generation_config,
- enabled_identifiers=enabled_identifiers,
- enable_parallel_agentic_issue_identification=enable_parallel_agentic_issue_identification,
- max_identify_workers=max_identify_workers,
- filter_issues=filter_issues,
- filter_issues_below_confidence=filter_issues_below_confidence,
- enable_deduplication=enable_deduplication,
- enable_collation=enable_collation,
- enabled_issue_codes=enabled_issue_codes,
- temperature=temperature,
- cache_full_prompt=cache_full_prompt,
- )
-
-
-def get_enabled_issue_codes(config: VetConfig) -> set[IssueCode]:
- all_issue_code_values = {item.value for item in IssueCode}
- explicitly_enabled = config.enabled_issue_codes or tuple()
- explicitly_disabled = config.disabled_issue_codes or tuple()
- for code in explicitly_enabled + explicitly_disabled:
- if code not in all_issue_code_values:
- raise ValueError(f"Bad config: unknown issue code: {code}")
- possibly_enabled_values = set(explicitly_enabled) if len(explicitly_enabled) > 0 else set(v for v in IssueCode)
- disabled_values = set(explicitly_disabled)
- return possibly_enabled_values - disabled_values
diff --git a/imbue_tools/imbue_tools/util_prompts/goal_from_conversation.py b/imbue_tools/imbue_tools/util_prompts/goal_from_conversation.py
@@ -1,64 +0,0 @@
-import jinja2
-
-from imbue_core.agents.configs import LanguageModelGenerationConfig
-from imbue_core.agents.llm_apis.build_apis import build_language_model_from_config
-from imbue_core.agents.llm_apis.data_types import CostedLanguageModelResponse
-from imbue_core.agents.llm_apis.data_types import LanguageModelGenerationParams
-from imbue_core.itertools import only
-from vet_types.messages import ConversationMessageUnion
-from imbue_tools.get_conversation_history.get_conversation_history import (
- format_conversation_history_for_prompt,
-)
-from imbue_tools.util_prompts.conversation_prefix import CONVERSATION_PREFIX_TEMPLATE
-
-# TODO: see how this does on actual examples where the agent did something other than what the user asked for
-PROMPT_TEMPLATE = (
- CONVERSATION_PREFIX_TEMPLATE
- + """
-[ROLE=USER]
-What is the user's goal based on the preceding conversation?
-Pay attention only to what the user asks for, not what the agent does.
-Respond with a brief description of the goal--a few sentences at most.
-The goal should be listed as an imperative; for example "Implement XYZ" rather than "The user's goal is to implement XYZ".
-Do not include any reasoning or other text in your response.
-"""
-)
-
-# should be totally sufficient for a goal that's only supposed to be a few sentences
-MAX_OUTPUT_TOKENS = 500
-
-GOAL_GENERATION_DEFAULT_PARAMS = LanguageModelGenerationParams(temperature=0.0, max_tokens=MAX_OUTPUT_TOKENS)
-
-
-def prompt_for_getting_goal_from_conversation(
- conversation_history: tuple[ConversationMessageUnion, ...],
-) -> str:
- env = jinja2.Environment(undefined=jinja2.StrictUndefined)
- jinja_template = env.from_string(PROMPT_TEMPLATE)
- return jinja_template.render(conversation_history=format_conversation_history_for_prompt(conversation_history))
-
-
-def get_goal_from_conversation_with_usage(
- conversation_history: tuple[ConversationMessageUnion, ...],
- language_model_generation_config: LanguageModelGenerationConfig,
-) -> CostedLanguageModelResponse:
- """Query an LLM with the conversation history to get the user's goal, and include usage info in the response."""
- language_model = build_language_model_from_config(language_model_generation_config)
- prompt = prompt_for_getting_goal_from_conversation(conversation_history)
- costed_response = language_model.complete_with_usage_sync(
- prompt,
- params=GOAL_GENERATION_DEFAULT_PARAMS,
- is_caching_enabled=language_model.cache_path is not None,
- )
- return costed_response
-
-
-def get_goal_from_conversation(
- conversation_history: tuple[ConversationMessageUnion, ...],
- language_model_generation_config: LanguageModelGenerationConfig,
-) -> str:
- """Query an LLM with the conversation history to get the user's goal."""
- response = only(
- get_goal_from_conversation_with_usage(conversation_history, language_model_generation_config).responses
- )
- return response.text
diff --git a/imbue_tools/pyproject.toml b/imbue_tools/pyproject.toml
@@ -1,43 +0,0 @@
-[build-system]
-requires = ["setuptools", "wheel"]
-build-backend = "setuptools.build_meta"
-
-[project]
-name = "imbue-tools"
-version = "0.1.0"
-description = "Utils for imbue-cli tools"
-readme = "README.md"
-dependencies = [
- "anyio",
- "async_lru",
- "attrs",
- "imbue_core",
- "jinja2",
- "libcst",
- "loguru",
- "psycopg[binary]",
- "pydantic",
- "pydantic-settings",
- "pygit2",
- "pytest",
- "pytest-asyncio",
- "python-gitlab",
- "requests",
- "syrupy",
-]
-requires-python = ">=3.11"
-
-[project.optional-dependencies]
-test = [
- "vet",
-]
-
-[tool.setuptools]
-package-data.imbue_tools = ["py.typed"]
-
-[tool.setuptools.packages.find]
-include = ["imbue_tools*"]
-
-[tool.uv.sources]
-imbue_core = { path = "../imbue_core", editable = true }
-vet = { path = "..", editable = true }
diff --git a/imbue_tools/uv.lock b/imbue_tools/uv.lock
@@ -1,2960 +0,0 @@
-version = 1
-revision = 3
-requires-python = ">=3.11"
-resolution-markers = [
- "python_full_version >= '3.14'",
- "python_full_version == '3.13.*'",
- "python_full_version == '3.12.*'",
- "python_full_version < '3.12'",
-]
-
-[[package]]
-name = "aiohappyeyeballs"
-version = "2.6.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" },
-]
-
-[[package]]
-name = "aiohttp"
-version = "3.13.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "aiohappyeyeballs" },
- { name = "aiosignal" },
- { name = "attrs" },
- { name = "frozenlist" },
- { name = "multidict" },
- { name = "propcache" },
- { name = "yarl" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f1/4c/a164164834f03924d9a29dc3acd9e7ee58f95857e0b467f6d04298594ebb/aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b", size = 746051, upload-time = "2026-01-03T17:29:43.287Z" },
- { url = "https://files.pythonhosted.org/packages/82/71/d5c31390d18d4f58115037c432b7e0348c60f6f53b727cad33172144a112/aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64", size = 499234, upload-time = "2026-01-03T17:29:44.822Z" },
- { url = "https://files.pythonhosted.org/packages/0e/c9/741f8ac91e14b1d2e7100690425a5b2b919a87a5075406582991fb7de920/aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea", size = 494979, upload-time = "2026-01-03T17:29:46.405Z" },
- { url = "https://files.pythonhosted.org/packages/75/b5/31d4d2e802dfd59f74ed47eba48869c1c21552c586d5e81a9d0d5c2ad640/aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a", size = 1748297, upload-time = "2026-01-03T17:29:48.083Z" },
- { url = "https://files.pythonhosted.org/packages/1a/3e/eefad0ad42959f226bb79664826883f2687d602a9ae2941a18e0484a74d3/aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540", size = 1707172, upload-time = "2026-01-03T17:29:49.648Z" },
- { url = "https://files.pythonhosted.org/packages/c5/3a/54a64299fac2891c346cdcf2aa6803f994a2e4beeaf2e5a09dcc54acc842/aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b", size = 1805405, upload-time = "2026-01-03T17:29:51.244Z" },
- { url = "https://files.pythonhosted.org/packages/6c/70/ddc1b7169cf64075e864f64595a14b147a895a868394a48f6a8031979038/aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3", size = 1899449, upload-time = "2026-01-03T17:29:53.938Z" },
- { url = "https://files.pythonhosted.org/packages/a1/7e/6815aab7d3a56610891c76ef79095677b8b5be6646aaf00f69b221765021/aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1", size = 1748444, upload-time = "2026-01-03T17:29:55.484Z" },
- { url = "https://files.pythonhosted.org/packages/6b/f2/073b145c4100da5511f457dc0f7558e99b2987cf72600d42b559db856fbc/aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3", size = 1606038, upload-time = "2026-01-03T17:29:57.179Z" },
- { url = "https://files.pythonhosted.org/packages/0a/c1/778d011920cae03ae01424ec202c513dc69243cf2db303965615b81deeea/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440", size = 1724156, upload-time = "2026-01-03T17:29:58.914Z" },
- { url = "https://files.pythonhosted.org/packages/0e/cb/3419eabf4ec1e9ec6f242c32b689248365a1cf621891f6f0386632525494/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7", size = 1722340, upload-time = "2026-01-03T17:30:01.962Z" },
- { url = "https://files.pythonhosted.org/packages/7a/e5/76cf77bdbc435bf233c1f114edad39ed4177ccbfab7c329482b179cff4f4/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c", size = 1783041, upload-time = "2026-01-03T17:30:03.609Z" },
- { url = "https://files.pythonhosted.org/packages/9d/d4/dd1ca234c794fd29c057ce8c0566b8ef7fd6a51069de5f06fa84b9a1971c/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51", size = 1596024, upload-time = "2026-01-03T17:30:05.132Z" },
- { url = "https://files.pythonhosted.org/packages/55/58/4345b5f26661a6180afa686c473620c30a66afdf120ed3dd545bbc809e85/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4", size = 1804590, upload-time = "2026-01-03T17:30:07.135Z" },
- { url = "https://files.pythonhosted.org/packages/7b/06/05950619af6c2df7e0a431d889ba2813c9f0129cec76f663e547a5ad56f2/aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29", size = 1740355, upload-time = "2026-01-03T17:30:09.083Z" },
- { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701, upload-time = "2026-01-03T17:30:10.869Z" },
- { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678, upload-time = "2026-01-03T17:30:12.719Z" },
- { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" },
- { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" },
- { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" },
- { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" },
- { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" },
- { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" },
- { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" },
- { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" },
- { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" },
- { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" },
- { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" },
- { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" },
- { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" },
- { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" },
- { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" },
- { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" },
- { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" },
- { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" },
- { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" },
- { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" },
- { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" },
- { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" },
- { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" },
- { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" },
- { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" },
- { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" },
- { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" },
- { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" },
- { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" },
- { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" },
- { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" },
- { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" },
- { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" },
- { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" },
- { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" },
- { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" },
- { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" },
- { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" },
- { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" },
- { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" },
- { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" },
- { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" },
- { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" },
- { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" },
- { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" },
- { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" },
- { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" },
- { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" },
- { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" },
- { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" },
- { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" },
- { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" },
- { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" },
- { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" },
- { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" },
- { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" },
- { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" },
- { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" },
- { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" },
- { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" },
- { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" },
- { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" },
- { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" },
- { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" },
- { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" },
- { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" },
- { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" },
- { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" },
-]
-
-[[package]]
-name = "aiosignal"
-version = "1.4.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "frozenlist" },
- { name = "typing-extensions", marker = "python_full_version < '3.13'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
-]
-
-[[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.76.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/6e/be/d11abafaa15d6304826438170f7574d750218f49a106c54424a40cef4494/anthropic-0.76.0.tar.gz", hash = "sha256:e0cae6a368986d5cf6df743dfbb1b9519e6a9eee9c6c942ad8121c0b34416ffe", size = 495483, upload-time = "2026-01-13T18:41:14.908Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e5/70/7b0fd9c1a738f59d3babe2b4212031c34ab7d0fda4ffef15b58a55c5bcea/anthropic-0.76.0-py3-none-any.whl", hash = "sha256:81efa3113901192af2f0fe977d3ec73fdadb1e691586306c4256cd6d5ccc331c", size = 390309, upload-time = "2026-01-13T18:41:13.483Z" },
-]
-
-[[package]]
-name = "anyio"
-version = "4.12.1"
-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/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
-]
-
-[[package]]
-name = "astroid"
-version = "3.2.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/9e/53/1067e1113ecaf58312357f2cd93063674924119d80d173adc3f6f2387aa2/astroid-3.2.4.tar.gz", hash = "sha256:0e14202810b30da1b735827f78f5157be2bbd4a7a59b7707ca0bfc2fb4c0063a", size = 397576, upload-time = "2024-07-20T12:57:43.26Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/80/96/b32bbbb46170a1c8b8b1f28c794202e25cfe743565e9d3469b8eb1e0cc05/astroid-3.2.4-py3-none-any.whl", hash = "sha256:413658a61eeca6202a59231abb473f932038fbcbf1666587f66d482083413a25", size = 276348, upload-time = "2024-07-20T12:57:40.886Z" },
-]
-
-[[package]]
-name = "asttokens"
-version = "3.0.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" },
-]
-
-[[package]]
-name = "async-lru"
-version = "2.1.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ef/c3/bbf34f15ea88dfb649ab2c40f9d75081784a50573a9ea431563cab64adb8/async_lru-2.1.0.tar.gz", hash = "sha256:9eeb2fecd3fe42cc8a787fc32ead53a3a7158cc43d039c3c55ab3e4e5b2a80ed", size = 12041, upload-time = "2026-01-17T22:52:18.931Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2e/e9/eb6a5db5ac505d5d45715388e92bced7a5bb556facc4d0865d192823f2d2/async_lru-2.1.0-py3-none-any.whl", hash = "sha256:fa12dcf99a42ac1280bc16c634bbaf06883809790f6304d85cdab3f666f33a7e", size = 6933, upload-time = "2026-01-17T22:52:17.389Z" },
-]
-
-[[package]]
-name = "attrs"
-version = "25.4.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
-]
-
-[[package]]
-name = "black"
-version = "25.12.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "click" },
- { name = "mypy-extensions" },
- { name = "packaging" },
- { name = "pathspec" },
- { name = "platformdirs" },
- { name = "pytokens" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/c4/d9/07b458a3f1c525ac392b5edc6b191ff140b596f9d77092429417a54e249d/black-25.12.0.tar.gz", hash = "sha256:8d3dd9cea14bff7ddc0eb243c811cdb1a011ebb4800a5f0335a01a68654796a7", size = 659264, upload-time = "2025-12-08T01:40:52.501Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/60/ad/7ac0d0e1e0612788dbc48e62aef8a8e8feffac7eb3d787db4e43b8462fa8/black-25.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0cfa263e85caea2cff57d8f917f9f51adae8e20b610e2b23de35b5b11ce691a", size = 1877003, upload-time = "2025-12-08T01:43:29.967Z" },
- { url = "https://files.pythonhosted.org/packages/e8/dd/a237e9f565f3617a88b49284b59cbca2a4f56ebe68676c1aad0ce36a54a7/black-25.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a2f578ae20c19c50a382286ba78bfbeafdf788579b053d8e4980afb079ab9be", size = 1712639, upload-time = "2025-12-08T01:52:46.756Z" },
- { url = "https://files.pythonhosted.org/packages/12/80/e187079df1ea4c12a0c63282ddd8b81d5107db6d642f7d7b75a6bcd6fc21/black-25.12.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e1b65634b0e471d07ff86ec338819e2ef860689859ef4501ab7ac290431f9b", size = 1758143, upload-time = "2025-12-08T01:45:29.137Z" },
- { url = "https://files.pythonhosted.org/packages/93/b5/3096ccee4f29dc2c3aac57274326c4d2d929a77e629f695f544e159bfae4/black-25.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a3fa71e3b8dd9f7c6ac4d818345237dfb4175ed3bf37cd5a581dbc4c034f1ec5", size = 1420698, upload-time = "2025-12-08T01:45:53.379Z" },
- { url = "https://files.pythonhosted.org/packages/7e/39/f81c0ffbc25ffbe61c7d0385bf277e62ffc3e52f5ee668d7369d9854fadf/black-25.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:51e267458f7e650afed8445dc7edb3187143003d52a1b710c7321aef22aa9655", size = 1229317, upload-time = "2025-12-08T01:46:35.606Z" },
- { url = "https://files.pythonhosted.org/packages/d1/bd/26083f805115db17fda9877b3c7321d08c647df39d0df4c4ca8f8450593e/black-25.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31f96b7c98c1ddaeb07dc0f56c652e25bdedaac76d5b68a059d998b57c55594a", size = 1924178, upload-time = "2025-12-08T01:49:51.048Z" },
- { url = "https://files.pythonhosted.org/packages/89/6b/ea00d6651561e2bdd9231c4177f4f2ae19cc13a0b0574f47602a7519b6ca/black-25.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05dd459a19e218078a1f98178c13f861fe6a9a5f88fc969ca4d9b49eb1809783", size = 1742643, upload-time = "2025-12-08T01:49:59.09Z" },
- { url = "https://files.pythonhosted.org/packages/6d/f3/360fa4182e36e9875fabcf3a9717db9d27a8d11870f21cff97725c54f35b/black-25.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1f68c5eff61f226934be6b5b80296cf6939e5d2f0c2f7d543ea08b204bfaf59", size = 1800158, upload-time = "2025-12-08T01:44:27.301Z" },
- { url = "https://files.pythonhosted.org/packages/f8/08/2c64830cb6616278067e040acca21d4f79727b23077633953081c9445d61/black-25.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:274f940c147ddab4442d316b27f9e332ca586d39c85ecf59ebdea82cc9ee8892", size = 1426197, upload-time = "2025-12-08T01:45:51.198Z" },
- { url = "https://files.pythonhosted.org/packages/d4/60/a93f55fd9b9816b7432cf6842f0e3000fdd5b7869492a04b9011a133ee37/black-25.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:169506ba91ef21e2e0591563deda7f00030cb466e747c4b09cb0a9dae5db2f43", size = 1237266, upload-time = "2025-12-08T01:45:10.556Z" },
- { url = "https://files.pythonhosted.org/packages/c8/52/c551e36bc95495d2aa1a37d50566267aa47608c81a53f91daa809e03293f/black-25.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a05ddeb656534c3e27a05a29196c962877c83fa5503db89e68857d1161ad08a5", size = 1923809, upload-time = "2025-12-08T01:46:55.126Z" },
- { url = "https://files.pythonhosted.org/packages/a0/f7/aac9b014140ee56d247e707af8db0aae2e9efc28d4a8aba92d0abd7ae9d1/black-25.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ec77439ef3e34896995503865a85732c94396edcc739f302c5673a2315e1e7f", size = 1742384, upload-time = "2025-12-08T01:49:37.022Z" },
- { url = "https://files.pythonhosted.org/packages/74/98/38aaa018b2ab06a863974c12b14a6266badc192b20603a81b738c47e902e/black-25.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e509c858adf63aa61d908061b52e580c40eae0dfa72415fa47ac01b12e29baf", size = 1798761, upload-time = "2025-12-08T01:46:05.386Z" },
- { url = "https://files.pythonhosted.org/packages/16/3a/a8ac542125f61574a3f015b521ca83b47321ed19bb63fe6d7560f348bfe1/black-25.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:252678f07f5bac4ff0d0e9b261fbb029fa530cfa206d0a636a34ab445ef8ca9d", size = 1429180, upload-time = "2025-12-08T01:45:34.903Z" },
- { url = "https://files.pythonhosted.org/packages/e6/2d/bdc466a3db9145e946762d52cd55b1385509d9f9004fec1c97bdc8debbfb/black-25.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc5b1c09fe3c931ddd20ee548511c64ebf964ada7e6f0763d443947fd1c603ce", size = 1239350, upload-time = "2025-12-08T01:46:09.458Z" },
- { url = "https://files.pythonhosted.org/packages/35/46/1d8f2542210c502e2ae1060b2e09e47af6a5e5963cb78e22ec1a11170b28/black-25.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0a0953b134f9335c2434864a643c842c44fba562155c738a2a37a4d61f00cad5", size = 1917015, upload-time = "2025-12-08T01:53:27.987Z" },
- { url = "https://files.pythonhosted.org/packages/41/37/68accadf977672beb8e2c64e080f568c74159c1aaa6414b4cd2aef2d7906/black-25.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2355bbb6c3b76062870942d8cc450d4f8ac71f9c93c40122762c8784df49543f", size = 1741830, upload-time = "2025-12-08T01:54:36.861Z" },
- { url = "https://files.pythonhosted.org/packages/ac/76/03608a9d8f0faad47a3af3a3c8c53af3367f6c0dd2d23a84710456c7ac56/black-25.12.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9678bd991cc793e81d19aeeae57966ee02909877cb65838ccffef24c3ebac08f", size = 1791450, upload-time = "2025-12-08T01:44:52.581Z" },
- { url = "https://files.pythonhosted.org/packages/06/99/b2a4bd7dfaea7964974f947e1c76d6886d65fe5d24f687df2d85406b2609/black-25.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:97596189949a8aad13ad12fcbb4ae89330039b96ad6742e6f6b45e75ad5cfd83", size = 1452042, upload-time = "2025-12-08T01:46:13.188Z" },
- { url = "https://files.pythonhosted.org/packages/b2/7c/d9825de75ae5dd7795d007681b752275ea85a1c5d83269b4b9c754c2aaab/black-25.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:778285d9ea197f34704e3791ea9404cd6d07595745907dd2ce3da7a13627b29b", size = 1267446, upload-time = "2025-12-08T01:46:14.497Z" },
- { url = "https://files.pythonhosted.org/packages/68/11/21331aed19145a952ad28fca2756a1433ee9308079bd03bd898e903a2e53/black-25.12.0-py3-none-any.whl", hash = "sha256:48ceb36c16dbc84062740049eef990bb2ce07598272e673c17d1a7720c71c828", size = 206191, upload-time = "2025-12-08T01:40:50.963Z" },
-]
-
-[[package]]
-name = "boto3"
-version = "1.42.37"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "botocore" },
- { name = "jmespath" },
- { name = "s3transfer" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/a9/ef/0d6ceb88ae2b3638b956190a431e4a8a3697d5769d4bbbede8efcccacaea/boto3-1.42.37.tar.gz", hash = "sha256:d8b6c52c86f3bf04f71a5a53e7fb4d1527592afebffa5170cf3ef7d70966e610", size = 112830, upload-time = "2026-01-28T20:38:43.339Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/fb/a4/cd334f74498acc6ad42a69c48e8c495f6f721d8abe13f8ef0d4b862fb1c0/boto3-1.42.37-py3-none-any.whl", hash = "sha256:e1e38fd178ffc66cfbe9cb6838b8c460000c3eb741e5f40f57eb730780ef0ed4", size = 140604, upload-time = "2026-01-28T20:38:42.135Z" },
-]
-
-[[package]]
-name = "botocore"
-version = "1.42.37"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "jmespath" },
- { name = "python-dateutil" },
- { name = "urllib3" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/d5/4d/94292e7686e64d2ede8dae7102bbb11a1474e407c830de4192f2518e6cff/botocore-1.42.37.tar.gz", hash = "sha256:3ec58eb98b0857f67a2ae6aa3ded51597e7335f7640be654e0e86da4f173b5b2", size = 14914621, upload-time = "2026-01-28T20:38:34.586Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/72/30/54042dd3ad8161964f8f47aa418785079bd8d2f17053c40d65bafb9f6eed/botocore-1.42.37-py3-none-any.whl", hash = "sha256:f13bb8b560a10714d96fb7b0c7f17828dfa6e6606a1ead8c01c6ebb8765acbd8", size = 14589390, upload-time = "2026-01-28T20:38:31.306Z" },
-]
-
-[[package]]
-name = "cachetools"
-version = "6.2.6"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/39/91/d9ae9a66b01102a18cd16db0cf4cd54187ffe10f0865cc80071a4104fbb3/cachetools-6.2.6.tar.gz", hash = "sha256:16c33e1f276b9a9c0b49ab5782d901e3ad3de0dd6da9bf9bcd29ac5672f2f9e6", size = 32363, upload-time = "2026-01-27T20:32:59.956Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/90/45/f458fa2c388e79dd9d8b9b0c99f1d31b568f27388f2fdba7bb66bbc0c6ed/cachetools-6.2.6-py3-none-any.whl", hash = "sha256:8c9717235b3c651603fff0076db52d6acbfd1b338b8ed50256092f7ce9c85bda", size = 11668, upload-time = "2026-01-27T20:32:58.527Z" },
-]
-
-[[package]]
-name = "cattrs"
-version = "25.3.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "attrs" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/6e/00/2432bb2d445b39b5407f0a90e01b9a271475eea7caf913d7a86bcb956385/cattrs-25.3.0.tar.gz", hash = "sha256:1ac88d9e5eda10436c4517e390a4142d88638fe682c436c93db7ce4a277b884a", size = 509321, upload-time = "2025-10-07T12:26:08.737Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d8/2b/a40e1488fdfa02d3f9a653a61a5935ea08b3c2225ee818db6a76c7ba9695/cattrs-25.3.0-py3-none-any.whl", hash = "sha256:9896e84e0a5bf723bc7b4b68f4481785367ce07a8a02e7e9ee6eb2819bc306ff", size = 70738, upload-time = "2025-10-07T12:26:06.603Z" },
-]
-
-[[package]]
-name = "certifi"
-version = "2026.1.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
-]
-
-[[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 = "charset-normalizer"
-version = "3.4.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
- { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
- { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
- { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" },
- { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" },
- { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" },
- { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" },
- { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" },
- { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" },
- { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" },
- { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" },
- { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" },
- { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" },
- { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
- { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
- { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
- { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
- { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
- { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
- { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
- { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
- { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
- { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
- { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
- { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
- { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
- { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
- { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
- { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
- { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
- { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
- { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
- { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
- { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
- { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
- { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
- { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
- { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
- { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
- { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
- { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
- { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
- { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
- { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
- { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
- { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
- { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
- { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
- { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
- { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
- { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
- { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
- { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
- { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
- { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
- { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
- { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
- { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
- { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
- { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
- { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
- { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
- { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
- { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
- { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
-]
-
-[[package]]
-name = "click"
-version = "8.3.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
-]
-
-[[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 = "cryptography"
-version = "46.0.4"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/78/19/f748958276519adf6a0c1e79e7b8860b4830dda55ccdf29f2719b5fc499c/cryptography-46.0.4.tar.gz", hash = "sha256:bfd019f60f8abc2ed1b9be4ddc21cfef059c841d86d710bb69909a688cbb8f59", size = 749301, upload-time = "2026-01-28T00:24:37.379Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/8d/99/157aae7949a5f30d51fcb1a9851e8ebd5c74bf99b5285d8bb4b8b9ee641e/cryptography-46.0.4-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:281526e865ed4166009e235afadf3a4c4cba6056f99336a99efba65336fd5485", size = 7173686, upload-time = "2026-01-28T00:23:07.515Z" },
- { url = "https://files.pythonhosted.org/packages/87/91/874b8910903159043b5c6a123b7e79c4559ddd1896e38967567942635778/cryptography-46.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f14fba5bf6f4390d7ff8f086c566454bff0411f6d8aa7af79c88b6f9267aecc", size = 4275871, upload-time = "2026-01-28T00:23:09.439Z" },
- { url = "https://files.pythonhosted.org/packages/c0/35/690e809be77896111f5b195ede56e4b4ed0435b428c2f2b6d35046fbb5e8/cryptography-46.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47bcd19517e6389132f76e2d5303ded6cf3f78903da2158a671be8de024f4cd0", size = 4423124, upload-time = "2026-01-28T00:23:11.529Z" },
- { url = "https://files.pythonhosted.org/packages/1a/5b/a26407d4f79d61ca4bebaa9213feafdd8806dc69d3d290ce24996d3cfe43/cryptography-46.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:01df4f50f314fbe7009f54046e908d1754f19d0c6d3070df1e6268c5a4af09fa", size = 4277090, upload-time = "2026-01-28T00:23:13.123Z" },
- { url = "https://files.pythonhosted.org/packages/0c/d8/4bb7aec442a9049827aa34cee1aa83803e528fa55da9a9d45d01d1bb933e/cryptography-46.0.4-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5aa3e463596b0087b3da0dbe2b2487e9fc261d25da85754e30e3b40637d61f81", size = 4947652, upload-time = "2026-01-28T00:23:14.554Z" },
- { url = "https://files.pythonhosted.org/packages/2b/08/f83e2e0814248b844265802d081f2fac2f1cbe6cd258e72ba14ff006823a/cryptography-46.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0a9ad24359fee86f131836a9ac3bffc9329e956624a2d379b613f8f8abaf5255", size = 4455157, upload-time = "2026-01-28T00:23:16.443Z" },
- { url = "https://files.pythonhosted.org/packages/0a/05/19d849cf4096448779d2dcc9bb27d097457dac36f7273ffa875a93b5884c/cryptography-46.0.4-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:dc1272e25ef673efe72f2096e92ae39dea1a1a450dd44918b15351f72c5a168e", size = 3981078, upload-time = "2026-01-28T00:23:17.838Z" },
- { url = "https://files.pythonhosted.org/packages/e6/89/f7bac81d66ba7cde867a743ea5b37537b32b5c633c473002b26a226f703f/cryptography-46.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:de0f5f4ec8711ebc555f54735d4c673fc34b65c44283895f1a08c2b49d2fd99c", size = 4276213, upload-time = "2026-01-28T00:23:19.257Z" },
- { url = "https://files.pythonhosted.org/packages/da/9f/7133e41f24edd827020ad21b068736e792bc68eecf66d93c924ad4719fb3/cryptography-46.0.4-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:eeeb2e33d8dbcccc34d64651f00a98cb41b2dc69cef866771a5717e6734dfa32", size = 4912190, upload-time = "2026-01-28T00:23:21.244Z" },
- { url = "https://files.pythonhosted.org/packages/a6/f7/6d43cbaddf6f65b24816e4af187d211f0bc536a29961f69faedc48501d8e/cryptography-46.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:3d425eacbc9aceafd2cb429e42f4e5d5633c6f873f5e567077043ef1b9bbf616", size = 4454641, upload-time = "2026-01-28T00:23:22.866Z" },
- { url = "https://files.pythonhosted.org/packages/9e/4f/ebd0473ad656a0ac912a16bd07db0f5d85184924e14fc88feecae2492834/cryptography-46.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91627ebf691d1ea3976a031b61fb7bac1ccd745afa03602275dda443e11c8de0", size = 4405159, upload-time = "2026-01-28T00:23:25.278Z" },
- { url = "https://files.pythonhosted.org/packages/d1/f7/7923886f32dc47e27adeff8246e976d77258fd2aa3efdd1754e4e323bf49/cryptography-46.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0", size = 4666059, upload-time = "2026-01-28T00:23:26.766Z" },
- { url = "https://files.pythonhosted.org/packages/eb/a7/0fca0fd3591dffc297278a61813d7f661a14243dd60f499a7a5b48acb52a/cryptography-46.0.4-cp311-abi3-win32.whl", hash = "sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5", size = 3026378, upload-time = "2026-01-28T00:23:28.317Z" },
- { url = "https://files.pythonhosted.org/packages/2d/12/652c84b6f9873f0909374864a57b003686c642ea48c84d6c7e2c515e6da5/cryptography-46.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b", size = 3478614, upload-time = "2026-01-28T00:23:30.275Z" },
- { url = "https://files.pythonhosted.org/packages/b9/27/542b029f293a5cce59349d799d4d8484b3b1654a7b9a0585c266e974a488/cryptography-46.0.4-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:485e2b65d25ec0d901bca7bcae0f53b00133bf3173916d8e421f6fddde103908", size = 7116417, upload-time = "2026-01-28T00:23:31.958Z" },
- { url = "https://files.pythonhosted.org/packages/f8/f5/559c25b77f40b6bf828eabaf988efb8b0e17b573545edb503368ca0a2a03/cryptography-46.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:078e5f06bd2fa5aea5a324f2a09f914b1484f1d0c2a4d6a8a28c74e72f65f2da", size = 4264508, upload-time = "2026-01-28T00:23:34.264Z" },
- { url = "https://files.pythonhosted.org/packages/49/a1/551fa162d33074b660dc35c9bc3616fefa21a0e8c1edd27b92559902e408/cryptography-46.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dce1e4f068f03008da7fa51cc7abc6ddc5e5de3e3d1550334eaf8393982a5829", size = 4409080, upload-time = "2026-01-28T00:23:35.793Z" },
- { url = "https://files.pythonhosted.org/packages/b0/6a/4d8d129a755f5d6df1bbee69ea2f35ebfa954fa1847690d1db2e8bca46a5/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2067461c80271f422ee7bdbe79b9b4be54a5162e90345f86a23445a0cf3fd8a2", size = 4270039, upload-time = "2026-01-28T00:23:37.263Z" },
- { url = "https://files.pythonhosted.org/packages/4c/f5/ed3fcddd0a5e39321e595e144615399e47e7c153a1fb8c4862aec3151ff9/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:c92010b58a51196a5f41c3795190203ac52edfd5dc3ff99149b4659eba9d2085", size = 4926748, upload-time = "2026-01-28T00:23:38.884Z" },
- { url = "https://files.pythonhosted.org/packages/43/ae/9f03d5f0c0c00e85ecb34f06d3b79599f20630e4db91b8a6e56e8f83d410/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:829c2b12bbc5428ab02d6b7f7e9bbfd53e33efd6672d21341f2177470171ad8b", size = 4442307, upload-time = "2026-01-28T00:23:40.56Z" },
- { url = "https://files.pythonhosted.org/packages/8b/22/e0f9f2dae8040695103369cf2283ef9ac8abe4d51f68710bec2afd232609/cryptography-46.0.4-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:62217ba44bf81b30abaeda1488686a04a702a261e26f87db51ff61d9d3510abd", size = 3959253, upload-time = "2026-01-28T00:23:42.827Z" },
- { url = "https://files.pythonhosted.org/packages/01/5b/6a43fcccc51dae4d101ac7d378a8724d1ba3de628a24e11bf2f4f43cba4d/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:9c2da296c8d3415b93e6053f5a728649a87a48ce084a9aaf51d6e46c87c7f2d2", size = 4269372, upload-time = "2026-01-28T00:23:44.655Z" },
- { url = "https://files.pythonhosted.org/packages/17/b7/0f6b8c1dd0779df2b526e78978ff00462355e31c0a6f6cff8a3e99889c90/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9b34d8ba84454641a6bf4d6762d15847ecbd85c1316c0a7984e6e4e9f748ec2e", size = 4891908, upload-time = "2026-01-28T00:23:46.48Z" },
- { url = "https://files.pythonhosted.org/packages/83/17/259409b8349aa10535358807a472c6a695cf84f106022268d31cea2b6c97/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:df4a817fa7138dd0c96c8c8c20f04b8aaa1fac3bbf610913dcad8ea82e1bfd3f", size = 4441254, upload-time = "2026-01-28T00:23:48.403Z" },
- { url = "https://files.pythonhosted.org/packages/9c/fe/e4a1b0c989b00cee5ffa0764401767e2d1cf59f45530963b894129fd5dce/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b1de0ebf7587f28f9190b9cb526e901bf448c9e6a99655d2b07fff60e8212a82", size = 4396520, upload-time = "2026-01-28T00:23:50.26Z" },
- { url = "https://files.pythonhosted.org/packages/b3/81/ba8fd9657d27076eb40d6a2f941b23429a3c3d2f56f5a921d6b936a27bc9/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9b4d17bc7bd7cdd98e3af40b441feaea4c68225e2eb2341026c84511ad246c0c", size = 4651479, upload-time = "2026-01-28T00:23:51.674Z" },
- { url = "https://files.pythonhosted.org/packages/00/03/0de4ed43c71c31e4fe954edd50b9d28d658fef56555eba7641696370a8e2/cryptography-46.0.4-cp314-cp314t-win32.whl", hash = "sha256:c411f16275b0dea722d76544a61d6421e2cc829ad76eec79280dbdc9ddf50061", size = 3001986, upload-time = "2026-01-28T00:23:53.485Z" },
- { url = "https://files.pythonhosted.org/packages/5c/70/81830b59df7682917d7a10f833c4dab2a5574cd664e86d18139f2b421329/cryptography-46.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:728fedc529efc1439eb6107b677f7f7558adab4553ef8669f0d02d42d7b959a7", size = 3468288, upload-time = "2026-01-28T00:23:55.09Z" },
- { url = "https://files.pythonhosted.org/packages/56/f7/f648fdbb61d0d45902d3f374217451385edc7e7768d1b03ff1d0e5ffc17b/cryptography-46.0.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab", size = 7169583, upload-time = "2026-01-28T00:23:56.558Z" },
- { url = "https://files.pythonhosted.org/packages/d8/cc/8f3224cbb2a928de7298d6ed4790f5ebc48114e02bdc9559196bfb12435d/cryptography-46.0.4-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef", size = 4275419, upload-time = "2026-01-28T00:23:58.364Z" },
- { url = "https://files.pythonhosted.org/packages/17/43/4a18faa7a872d00e4264855134ba82d23546c850a70ff209e04ee200e76f/cryptography-46.0.4-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d", size = 4419058, upload-time = "2026-01-28T00:23:59.867Z" },
- { url = "https://files.pythonhosted.org/packages/ee/64/6651969409821d791ba12346a124f55e1b76f66a819254ae840a965d4b9c/cryptography-46.0.4-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:812815182f6a0c1d49a37893a303b44eaac827d7f0d582cecfc81b6427f22973", size = 4278151, upload-time = "2026-01-28T00:24:01.731Z" },
- { url = "https://files.pythonhosted.org/packages/20/0b/a7fce65ee08c3c02f7a8310cc090a732344066b990ac63a9dfd0a655d321/cryptography-46.0.4-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a90e43e3ef65e6dcf969dfe3bb40cbf5aef0d523dff95bfa24256be172a845f4", size = 4939441, upload-time = "2026-01-28T00:24:03.175Z" },
- { url = "https://files.pythonhosted.org/packages/db/a7/20c5701e2cd3e1dfd7a19d2290c522a5f435dd30957d431dcb531d0f1413/cryptography-46.0.4-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a05177ff6296644ef2876fce50518dffb5bcdf903c85250974fc8bc85d54c0af", size = 4451617, upload-time = "2026-01-28T00:24:05.403Z" },
- { url = "https://files.pythonhosted.org/packages/00/dc/3e16030ea9aa47b63af6524c354933b4fb0e352257c792c4deeb0edae367/cryptography-46.0.4-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:daa392191f626d50f1b136c9b4cf08af69ca8279d110ea24f5c2700054d2e263", size = 3977774, upload-time = "2026-01-28T00:24:06.851Z" },
- { url = "https://files.pythonhosted.org/packages/42/c8/ad93f14118252717b465880368721c963975ac4b941b7ef88f3c56bf2897/cryptography-46.0.4-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e07ea39c5b048e085f15923511d8121e4a9dc45cee4e3b970ca4f0d338f23095", size = 4277008, upload-time = "2026-01-28T00:24:08.926Z" },
- { url = "https://files.pythonhosted.org/packages/00/cf/89c99698151c00a4631fbfcfcf459d308213ac29e321b0ff44ceeeac82f1/cryptography-46.0.4-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d5a45ddc256f492ce42a4e35879c5e5528c09cd9ad12420828c972951d8e016b", size = 4903339, upload-time = "2026-01-28T00:24:12.009Z" },
- { url = "https://files.pythonhosted.org/packages/03/c3/c90a2cb358de4ac9309b26acf49b2a100957e1ff5cc1e98e6c4996576710/cryptography-46.0.4-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:6bb5157bf6a350e5b28aee23beb2d84ae6f5be390b2f8ee7ea179cda077e1019", size = 4451216, upload-time = "2026-01-28T00:24:13.975Z" },
- { url = "https://files.pythonhosted.org/packages/96/2c/8d7f4171388a10208671e181ca43cdc0e596d8259ebacbbcfbd16de593da/cryptography-46.0.4-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd5aba870a2c40f87a3af043e0dee7d9eb02d4aff88a797b48f2b43eff8c3ab4", size = 4404299, upload-time = "2026-01-28T00:24:16.169Z" },
- { url = "https://files.pythonhosted.org/packages/e9/23/cbb2036e450980f65c6e0a173b73a56ff3bccd8998965dea5cc9ddd424a5/cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b", size = 4664837, upload-time = "2026-01-28T00:24:17.629Z" },
- { url = "https://files.pythonhosted.org/packages/0a/21/f7433d18fe6d5845329cbdc597e30caf983229c7a245bcf54afecc555938/cryptography-46.0.4-cp38-abi3-win32.whl", hash = "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc", size = 3009779, upload-time = "2026-01-28T00:24:20.198Z" },
- { url = "https://files.pythonhosted.org/packages/3a/6a/bd2e7caa2facffedf172a45c1a02e551e6d7d4828658c9a245516a598d94/cryptography-46.0.4-cp38-abi3-win_amd64.whl", hash = "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976", size = 3466633, upload-time = "2026-01-28T00:24:21.851Z" },
- { url = "https://files.pythonhosted.org/packages/59/e0/f9c6c53e1f2a1c2507f00f2faba00f01d2f334b35b0fbfe5286715da2184/cryptography-46.0.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:766330cce7416c92b5e90c3bb71b1b79521760cdcfc3a6a1a182d4c9fab23d2b", size = 3476316, upload-time = "2026-01-28T00:24:24.144Z" },
- { url = "https://files.pythonhosted.org/packages/27/7a/f8d2d13227a9a1a9fe9c7442b057efecffa41f1e3c51d8622f26b9edbe8f/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c236a44acfb610e70f6b3e1c3ca20ff24459659231ef2f8c48e879e2d32b73da", size = 4216693, upload-time = "2026-01-28T00:24:25.758Z" },
- { url = "https://files.pythonhosted.org/packages/c5/de/3787054e8f7972658370198753835d9d680f6cd4a39df9f877b57f0dd69c/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8a15fb869670efa8f83cbffbc8753c1abf236883225aed74cd179b720ac9ec80", size = 4382765, upload-time = "2026-01-28T00:24:27.577Z" },
- { url = "https://files.pythonhosted.org/packages/8a/5f/60e0afb019973ba6a0b322e86b3d61edf487a4f5597618a430a2a15f2d22/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:fdc3daab53b212472f1524d070735b2f0c214239df131903bae1d598016fa822", size = 4216066, upload-time = "2026-01-28T00:24:29.056Z" },
- { url = "https://files.pythonhosted.org/packages/81/8e/bf4a0de294f147fee66f879d9bae6f8e8d61515558e3d12785dd90eca0be/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:44cc0675b27cadb71bdbb96099cca1fa051cd11d2ade09e5cd3a2edb929ed947", size = 4382025, upload-time = "2026-01-28T00:24:30.681Z" },
- { url = "https://files.pythonhosted.org/packages/79/f4/9ceb90cfd6a3847069b0b0b353fd3075dc69b49defc70182d8af0c4ca390/cryptography-46.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be8c01a7d5a55f9a47d1888162b76c8f49d62b234d88f0ff91a9fbebe32ffbc3", size = 3406043, upload-time = "2026-01-28T00:24:32.236Z" },
-]
-
-[[package]]
-name = "dill"
-version = "0.4.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" },
-]
-
-[[package]]
-name = "diskcache"
-version = "5.6.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" },
-]
-
-[[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.17.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" },
-]
-
-[[package]]
-name = "eval-type-backport"
-version = "0.2.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/30/ea/8b0ac4469d4c347c6a385ff09dc3c048c2d021696664e26c7ee6791631b5/eval_type_backport-0.2.2.tar.gz", hash = "sha256:f0576b4cf01ebb5bd358d02314d31846af5e07678387486e2c798af0e7d849c1", size = 9079, upload-time = "2024-12-21T20:09:46.005Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ce/31/55cd413eaccd39125368be33c46de24a1f639f2e12349b0361b4678f3915/eval_type_backport-0.2.2-py3-none-any.whl", hash = "sha256:cb6ad7c393517f476f96d456d0412ea80f0a8cf96f6892834cd9340149111b0a", size = 5830, upload-time = "2024-12-21T20:09:44.175Z" },
-]
-
-[[package]]
-name = "executing"
-version = "2.2.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" },
-]
-
-[[package]]
-name = "filelock"
-version = "3.20.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" },
-]
-
-[[package]]
-name = "frozenlist"
-version = "1.8.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" },
- { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" },
- { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" },
- { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" },
- { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" },
- { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" },
- { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" },
- { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" },
- { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" },
- { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" },
- { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" },
- { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" },
- { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" },
- { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" },
- { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" },
- { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" },
- { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" },
- { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" },
- { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" },
- { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" },
- { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" },
- { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" },
- { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" },
- { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" },
- { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" },
- { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" },
- { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" },
- { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" },
- { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" },
- { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" },
- { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" },
- { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" },
- { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" },
- { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" },
- { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" },
- { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" },
- { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" },
- { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" },
- { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" },
- { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" },
- { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" },
- { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" },
- { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" },
- { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" },
- { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" },
- { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" },
- { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" },
- { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" },
- { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" },
- { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" },
- { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" },
- { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" },
- { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" },
- { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" },
- { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" },
- { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" },
- { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" },
- { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" },
- { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" },
- { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" },
- { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" },
- { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" },
- { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" },
- { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" },
- { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" },
- { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" },
- { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" },
- { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" },
- { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" },
- { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" },
- { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" },
- { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" },
- { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" },
- { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" },
- { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" },
- { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" },
- { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" },
- { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" },
- { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" },
- { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" },
- { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" },
- { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" },
- { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" },
- { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" },
- { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" },
- { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" },
- { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" },
- { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" },
- { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" },
- { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" },
- { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" },
- { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" },
- { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" },
- { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" },
- { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" },
- { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" },
- { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
-]
-
-[[package]]
-name = "google-auth"
-version = "2.48.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "cryptography" },
- { name = "pyasn1-modules" },
- { name = "rsa" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/0c/41/242044323fbd746615884b1c16639749e73665b718209946ebad7ba8a813/google_auth-2.48.0.tar.gz", hash = "sha256:4f7e706b0cd3208a3d940a19a822c37a476ddba5450156c3e6624a71f7c841ce", size = 326522, upload-time = "2026-01-26T19:22:47.157Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f", size = 236499, upload-time = "2026-01-26T19:22:45.099Z" },
-]
-
-[package.optional-dependencies]
-requests = [
- { name = "requests" },
-]
-
-[[package]]
-name = "google-genai"
-version = "1.60.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/0a/3f/a753be0dcee352b7d63bc6d1ba14a72591d63b6391dac0cdff7ac168c530/google_genai-1.60.0.tar.gz", hash = "sha256:9768061775fddfaecfefb0d6d7a6cabefb3952ebd246cd5f65247151c07d33d1", size = 487721, upload-time = "2026-01-21T22:17:30.398Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/31/e5/384b1f383917b5f0ae92e28f47bc27b16e3d26cd9bacb25e9f8ecab3c8fe/google_genai-1.60.0-py3-none-any.whl", hash = "sha256:967338378ffecebec19a8ed90cf8797b26818bacbefd7846a9280beb1099f7f3", size = 719431, upload-time = "2026-01-21T22:17:28.086Z" },
-]
-
-[[package]]
-name = "groq"
-version = "1.0.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "anyio" },
- { name = "distro" },
- { name = "httpx" },
- { name = "pydantic" },
- { name = "sniffio" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/3f/12/f4099a141677fcd2ed79dcc1fcec431e60c52e0e90c9c5d935f0ffaf8c0e/groq-1.0.0.tar.gz", hash = "sha256:66cb7bb729e6eb644daac7ce8efe945e99e4eb33657f733ee6f13059ef0c25a9", size = 146068, upload-time = "2025-12-17T23:34:23.115Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/4a/88/3175759d2ef30406ea721f4d837bfa1ba4339fde3b81ba8c5640a96ed231/groq-1.0.0-py3-none-any.whl", hash = "sha256:6e22bf92ffad988f01d2d4df7729add66b8fd5dbfb2154b5bbf3af245b72c731", size = 138292, upload-time = "2025-12-17T23:34:21.957Z" },
-]
-
-[[package]]
-name = "grpclib"
-version = "0.4.9"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "h2" },
- { name = "multidict" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/5b/28/5a2c299ec82a876a252c5919aa895a6f1d1d35c96417c5ce4a4660dc3a80/grpclib-0.4.9.tar.gz", hash = "sha256:cc589c330fa81004c6400a52a566407574498cb5b055fa927013361e21466c46", size = 84798, upload-time = "2025-12-14T22:23:14.349Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/5c/90/b0cbbd9efcc82816c58f31a34963071aa19fb792a212a5d9caf8e0fc3097/grpclib-0.4.9-py3-none-any.whl", hash = "sha256:7762ec1c8ed94dfad597475152dd35cbd11aecaaca2f243e29702435ca24cf0e", size = 77063, upload-time = "2025-12-14T22:23:13.224Z" },
-]
-
-[[package]]
-name = "h11"
-version = "0.16.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
-]
-
-[[package]]
-name = "h2"
-version = "4.3.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "hpack" },
- { name = "hyperframe" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" },
-]
-
-[[package]]
-name = "hpack"
-version = "4.1.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" },
-]
-
-[[package]]
-name = "httpcore"
-version = "1.0.9"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "certifi" },
- { name = "h11" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
-]
-
-[[package]]
-name = "httpx"
-version = "0.28.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "anyio" },
- { name = "certifi" },
- { name = "httpcore" },
- { name = "idna" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
-]
-
-[[package]]
-name = "hyperframe"
-version = "6.1.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" },
-]
-
-[[package]]
-name = "idna"
-version = "3.11"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
-]
-
-[[package]]
-name = "imbue-core"
-version = "0.0.9"
-source = { editable = "../imbue_core" }
-dependencies = [
- { name = "anthropic" },
- { name = "anyio" },
- { name = "attrs" },
- { name = "boto3" },
- { name = "cachetools" },
- { name = "cattrs" },
- { name = "diskcache" },
- { name = "google-genai" },
- { name = "groq" },
- { name = "grpclib" },
- { name = "httpx" },
- { name = "inline-snapshot" },
- { name = "loguru" },
- { name = "openai" },
- { name = "pathspec" },
- { name = "prometheus-client" },
- { name = "pydantic" },
- { name = "pydantic-settings" },
- { name = "pygit2" },
- { name = "pygments" },
- { name = "pyhumps" },
- { name = "pylint" },
- { name = "pytest" },
- { name = "pytest-asyncio" },
- { name = "pytest-mock" },
- { name = "python-gitlab" },
- { name = "syrupy" },
- { name = "tblib" },
- { name = "tenacity" },
- { name = "tiktoken" },
- { name = "together" },
- { name = "toml" },
- { name = "traceback-with-variables" },
- { name = "typeid-python" },
- { name = "yasoo" },
-]
-
-[package.metadata]
-requires-dist = [
- { name = "anthropic", specifier = "~=0.54" },
- { name = "anyio" },
- { name = "attrs" },
- { name = "boto3", specifier = ">=1.38.27" },
- { name = "cachetools" },
- { name = "cattrs" },
- { name = "diskcache", specifier = ">=5.6.3" },
- { name = "google-genai", specifier = ">=1.26.0" },
- { name = "groq", specifier = ">=0.18.0" },
- { name = "grpclib", specifier = ">=0.4.7" },
- { name = "httpx" },
- { name = "inline-snapshot" },
- { name = "loguru" },
- { name = "openai", specifier = ">=1.79.0" },
- { name = "pathspec" },
- { name = "prometheus-client", specifier = ">=0.20.0" },
- { name = "pydantic", specifier = ">=2.11.4" },
- { name = "pydantic-settings" },
- { name = "pygit2", specifier = ">=1.18.0" },
- { name = "pygments", specifier = ">=2.0.0" },
- { name = "pyhumps" },
- { name = "pylint", specifier = "==3.2.6" },
- { name = "pytest" },
- { name = "pytest-asyncio" },
- { name = "pytest-mock" },
- { name = "python-gitlab", specifier = ">=4.5.0" },
- { name = "syrupy" },
- { name = "tblib", specifier = "==2.0.0" },
- { name = "tenacity", specifier = ">=8.2.2" },
- { name = "tiktoken" },
- { name = "together" },
- { name = "toml" },
- { name = "traceback-with-variables", specifier = ">=2.2.0" },
- { name = "typeid-python" },
- { name = "yasoo" },
-]
-
-[package.metadata.requires-dev]
-dev = [{ name = "moto", specifier = ">=4.1.12" }]
-
-[[package]]
-name = "imbue-tools"
-version = "0.1.0"
-source = { editable = "." }
-dependencies = [
- { name = "anyio" },
- { name = "async-lru" },
- { name = "attrs" },
- { name = "imbue-core" },
- { name = "jinja2" },
- { name = "libcst" },
- { name = "loguru" },
- { name = "psycopg", extra = ["binary"] },
- { name = "pydantic" },
- { name = "pydantic-settings" },
- { name = "pygit2" },
- { name = "pytest" },
- { name = "pytest-asyncio" },
- { name = "python-gitlab" },
- { name = "requests" },
- { name = "syrupy" },
-]
-
-[package.optional-dependencies]
-test = [
- { name = "vet" },
-]
-
-[package.metadata]
-requires-dist = [
- { name = "anyio" },
- { name = "async-lru" },
- { name = "attrs" },
- { name = "imbue-core", editable = "../imbue_core" },
- { name = "jinja2" },
- { name = "libcst" },
- { name = "loguru" },
- { name = "psycopg", extras = ["binary"] },
- { name = "pydantic" },
- { name = "pydantic-settings" },
- { name = "pygit2" },
- { name = "pytest" },
- { name = "pytest-asyncio" },
- { name = "python-gitlab" },
- { name = "requests" },
- { name = "syrupy" },
- { name = "vet", marker = "extra == 'test'", editable = "../" },
-]
-provides-extras = ["test"]
-
-[[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 = "inline-snapshot"
-version = "0.31.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "asttokens" },
- { name = "executing" },
- { name = "pytest" },
- { name = "rich" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/1c/b1/52b5ee59f73ed31d5fe21b10881bf2d121d07d54b23c0b6b74186792e620/inline_snapshot-0.31.1.tar.gz", hash = "sha256:4ea5ed70aa1d652713bbfd750606b94bd8a42483f7d3680433b3e92994495f64", size = 2606338, upload-time = "2025-11-07T07:36:18.932Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ba/52/945db420380efbda8c69a7a4a16c53df9d7ac50d8217286b9d41e5d825ff/inline_snapshot-0.31.1-py3-none-any.whl", hash = "sha256:7875a73c986a03388c7e758fb5cb8a43d2c3a20328aa1d851bfb4ed536c4496f", size = 71965, upload-time = "2025-11-07T07:36:16.836Z" },
-]
-
-[[package]]
-name = "isort"
-version = "5.13.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/87/f9/c1eb8635a24e87ade2efce21e3ce8cd6b8630bb685ddc9cdaca1349b2eb5/isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", size = 175303, upload-time = "2023-12-13T20:37:26.124Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d1/b3/8def84f539e7d2289a02f0524b944b15d7c75dab7628bedf1c4f0992029c/isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6", size = 92310, upload-time = "2023-12-13T20:37:23.244Z" },
-]
-
-[[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.12.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435, upload-time = "2025-11-09T20:47:02.087Z" },
- { url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548, upload-time = "2025-11-09T20:47:03.409Z" },
- { url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915, upload-time = "2025-11-09T20:47:05.171Z" },
- { url = "https://files.pythonhosted.org/packages/15/2d/c06e659888c128ad1e838123d0638f0efad90cc30860cb5f74dd3f2fc0b3/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966, upload-time = "2025-11-09T20:47:06.508Z" },
- { url = "https://files.pythonhosted.org/packages/6b/20/058db4ae5fb07cf6a4ab2e9b9294416f606d8e467fb74c2184b2a1eeacba/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047, upload-time = "2025-11-09T20:47:08.382Z" },
- { url = "https://files.pythonhosted.org/packages/49/bb/dc2b1c122275e1de2eb12905015d61e8316b2f888bdaac34221c301495d6/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835, upload-time = "2025-11-09T20:47:09.81Z" },
- { url = "https://files.pythonhosted.org/packages/23/7d/38f9cd337575349de16da575ee57ddb2d5a64d425c9367f5ef9e4612e32e/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587, upload-time = "2025-11-09T20:47:11.529Z" },
- { url = "https://files.pythonhosted.org/packages/f0/a3/b13e8e61e70f0bb06085099c4e2462647f53cc2ca97614f7fedcaa2bb9f3/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492, upload-time = "2025-11-09T20:47:12.993Z" },
- { url = "https://files.pythonhosted.org/packages/07/71/e0d11422ed027e21422f7bc1883c61deba2d9752b720538430c1deadfbca/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046, upload-time = "2025-11-09T20:47:14.6Z" },
- { url = "https://files.pythonhosted.org/packages/9f/59/b968a9aa7102a8375dbbdfbd2aeebe563c7e5dddf0f47c9ef1588a97e224/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392, upload-time = "2025-11-09T20:47:16.011Z" },
- { url = "https://files.pythonhosted.org/packages/ca/e4/7df62002499080dbd61b505c5cb351aa09e9959d176cac2aa8da6f93b13b/jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c", size = 206096, upload-time = "2025-11-09T20:47:17.344Z" },
- { url = "https://files.pythonhosted.org/packages/bb/60/1032b30ae0572196b0de0e87dce3b6c26a1eff71aad5fe43dee3082d32e0/jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f", size = 204899, upload-time = "2025-11-09T20:47:19.365Z" },
- { url = "https://files.pythonhosted.org/packages/49/d5/c145e526fccdb834063fb45c071df78b0cc426bbaf6de38b0781f45d956f/jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5", size = 188070, upload-time = "2025-11-09T20:47:20.75Z" },
- { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" },
- { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" },
- { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" },
- { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" },
- { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" },
- { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" },
- { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" },
- { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" },
- { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" },
- { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" },
- { url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546, upload-time = "2025-11-09T20:47:40.47Z" },
- { url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196, upload-time = "2025-11-09T20:47:41.794Z" },
- { url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100, upload-time = "2025-11-09T20:47:43.007Z" },
- { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" },
- { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" },
- { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" },
- { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" },
- { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" },
- { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" },
- { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" },
- { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" },
- { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" },
- { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" },
- { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" },
- { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" },
- { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" },
- { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" },
- { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" },
- { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" },
- { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" },
- { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" },
- { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" },
- { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" },
- { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" },
- { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" },
- { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" },
- { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" },
- { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" },
- { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" },
- { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" },
- { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" },
- { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" },
- { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" },
- { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" },
- { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" },
- { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" },
- { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" },
- { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" },
- { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" },
- { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" },
- { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" },
- { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" },
- { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" },
- { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" },
- { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" },
- { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" },
- { url = "https://files.pythonhosted.org/packages/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144, upload-time = "2025-11-09T20:49:10.503Z" },
- { url = "https://files.pythonhosted.org/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877, upload-time = "2025-11-09T20:49:12.269Z" },
- { url = "https://files.pythonhosted.org/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419, upload-time = "2025-11-09T20:49:13.803Z" },
- { url = "https://files.pythonhosted.org/packages/21/01/857d4608f5edb0664aa791a3d45702e1a5bcfff9934da74035e7b9803846/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d", size = 347212, upload-time = "2025-11-09T20:49:15.643Z" },
- { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" },
- { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" },
- { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" },
- { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" },
-]
-
-[[package]]
-name = "jmespath"
-version = "1.1.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" },
-]
-
-[[package]]
-name = "libcst"
-version = "1.8.6"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pyyaml", marker = "python_full_version != '3.13.*'" },
- { name = "pyyaml-ft", marker = "python_full_version == '3.13.*'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/de/cd/337df968b38d94c5aabd3e1b10630f047a2b345f6e1d4456bd9fe7417537/libcst-1.8.6.tar.gz", hash = "sha256:f729c37c9317126da9475bdd06a7208eb52fcbd180a6341648b45a56b4ba708b", size = 891354, upload-time = "2025-11-03T22:33:30.621Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/dc/15/95c2ecadc0fb4af8a7057ac2012a4c0ad5921b9ef1ace6c20006b56d3b5f/libcst-1.8.6-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3649a813660fbffd7bc24d3f810b1f75ac98bd40d9d6f56d1f0ee38579021073", size = 2211289, upload-time = "2025-11-03T22:32:04.673Z" },
- { url = "https://files.pythonhosted.org/packages/80/c3/7e1107acd5ed15cf60cc07c7bb64498a33042dc4821874aea3ec4942f3cd/libcst-1.8.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0cbe17067055829607c5ba4afa46bfa4d0dd554c0b5a583546e690b7367a29b6", size = 2092927, upload-time = "2025-11-03T22:32:06.209Z" },
- { url = "https://files.pythonhosted.org/packages/c1/ff/0d2be87f67e2841a4a37d35505e74b65991d30693295c46fc0380ace0454/libcst-1.8.6-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:59a7e388c57d21d63722018978a8ddba7b176e3a99bd34b9b84a576ed53f2978", size = 2237002, upload-time = "2025-11-03T22:32:07.559Z" },
- { url = "https://files.pythonhosted.org/packages/69/99/8c4a1b35c7894ccd7d33eae01ac8967122f43da41325223181ca7e4738fe/libcst-1.8.6-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:b6c1248cc62952a3a005792b10cdef2a4e130847be9c74f33a7d617486f7e532", size = 2301048, upload-time = "2025-11-03T22:32:08.869Z" },
- { url = "https://files.pythonhosted.org/packages/9b/8b/d1aa811eacf936cccfb386ae0585aa530ea1221ccf528d67144e041f5915/libcst-1.8.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6421a930b028c5ef4a943b32a5a78b7f1bf15138214525a2088f11acbb7d3d64", size = 2300675, upload-time = "2025-11-03T22:32:10.579Z" },
- { url = "https://files.pythonhosted.org/packages/c6/6b/7b65cd41f25a10c1fef2389ddc5c2b2cc23dc4d648083fa3e1aa7e0eeac2/libcst-1.8.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6d8b67874f2188399a71a71731e1ba2d1a2c3173b7565d1cc7ffb32e8fbaba5b", size = 2407934, upload-time = "2025-11-03T22:32:11.856Z" },
- { url = "https://files.pythonhosted.org/packages/c5/8b/401cfff374bb3b785adfad78f05225225767ee190997176b2a9da9ed9460/libcst-1.8.6-cp311-cp311-win_amd64.whl", hash = "sha256:b0d8c364c44ae343937f474b2e492c1040df96d94530377c2f9263fb77096e4f", size = 2119247, upload-time = "2025-11-03T22:32:13.279Z" },
- { url = "https://files.pythonhosted.org/packages/f1/17/085f59eaa044b6ff6bc42148a5449df2b7f0ba567307de7782fe85c39ee2/libcst-1.8.6-cp311-cp311-win_arm64.whl", hash = "sha256:5dcaaebc835dfe5755bc85f9b186fb7e2895dda78e805e577fef1011d51d5a5c", size = 2001774, upload-time = "2025-11-03T22:32:14.647Z" },
- { url = "https://files.pythonhosted.org/packages/0c/3c/93365c17da3d42b055a8edb0e1e99f1c60c776471db6c9b7f1ddf6a44b28/libcst-1.8.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0c13d5bd3d8414a129e9dccaf0e5785108a4441e9b266e1e5e9d1f82d1b943c9", size = 2206166, upload-time = "2025-11-03T22:32:16.012Z" },
- { url = "https://files.pythonhosted.org/packages/1d/cb/7530940e6ac50c6dd6022349721074e19309eb6aa296e942ede2213c1a19/libcst-1.8.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f1472eeafd67cdb22544e59cf3bfc25d23dc94058a68cf41f6654ff4fcb92e09", size = 2083726, upload-time = "2025-11-03T22:32:17.312Z" },
- { url = "https://files.pythonhosted.org/packages/1b/cf/7e5eaa8c8f2c54913160671575351d129170db757bb5e4b7faffed022271/libcst-1.8.6-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:089c58e75cb142ec33738a1a4ea7760a28b40c078ab2fd26b270dac7d2633a4d", size = 2235755, upload-time = "2025-11-03T22:32:18.859Z" },
- { url = "https://files.pythonhosted.org/packages/55/54/570ec2b0e9a3de0af9922e3bb1b69a5429beefbc753a7ea770a27ad308bd/libcst-1.8.6-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c9d7aeafb1b07d25a964b148c0dda9451efb47bbbf67756e16eeae65004b0eb5", size = 2301473, upload-time = "2025-11-03T22:32:20.499Z" },
- { url = "https://files.pythonhosted.org/packages/11/4c/163457d1717cd12181c421a4cca493454bcabd143fc7e53313bc6a4ad82a/libcst-1.8.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:207481197afd328aa91d02670c15b48d0256e676ce1ad4bafb6dc2b593cc58f1", size = 2298899, upload-time = "2025-11-03T22:32:21.765Z" },
- { url = "https://files.pythonhosted.org/packages/35/1d/317ddef3669883619ef3d3395ea583305f353ef4ad87d7a5ac1c39be38e3/libcst-1.8.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:375965f34cc6f09f5f809244d3ff9bd4f6cb6699f571121cebce53622e7e0b86", size = 2408239, upload-time = "2025-11-03T22:32:23.275Z" },
- { url = "https://files.pythonhosted.org/packages/9a/a1/f47d8cccf74e212dd6044b9d6dbc223636508da99acff1d54786653196bc/libcst-1.8.6-cp312-cp312-win_amd64.whl", hash = "sha256:da95b38693b989eaa8d32e452e8261cfa77fe5babfef1d8d2ac25af8c4aa7e6d", size = 2119660, upload-time = "2025-11-03T22:32:24.822Z" },
- { url = "https://files.pythonhosted.org/packages/19/d0/dd313bf6a7942cdf951828f07ecc1a7695263f385065edc75ef3016a3cb5/libcst-1.8.6-cp312-cp312-win_arm64.whl", hash = "sha256:bff00e1c766658adbd09a175267f8b2f7616e5ee70ce45db3d7c4ce6d9f6bec7", size = 1999824, upload-time = "2025-11-03T22:32:26.131Z" },
- { url = "https://files.pythonhosted.org/packages/90/01/723cd467ec267e712480c772aacc5aa73f82370c9665162fd12c41b0065b/libcst-1.8.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7445479ebe7d1aff0ee094ab5a1c7718e1ad78d33e3241e1a1ec65dcdbc22ffb", size = 2206386, upload-time = "2025-11-03T22:32:27.422Z" },
- { url = "https://files.pythonhosted.org/packages/17/50/b944944f910f24c094f9b083f76f61e3985af5a376f5342a21e01e2d1a81/libcst-1.8.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4fc3fef8a2c983e7abf5d633e1884c5dd6fa0dcb8f6e32035abd3d3803a3a196", size = 2083945, upload-time = "2025-11-03T22:32:28.847Z" },
- { url = "https://files.pythonhosted.org/packages/36/a1/bd1b2b2b7f153d82301cdaddba787f4a9fc781816df6bdb295ca5f88b7cf/libcst-1.8.6-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:1a3a5e4ee870907aa85a4076c914ae69066715a2741b821d9bf16f9579de1105", size = 2235818, upload-time = "2025-11-03T22:32:30.504Z" },
- { url = "https://files.pythonhosted.org/packages/b9/ab/f5433988acc3b4d188c4bb154e57837df9488cc9ab551267cdeabd3bb5e7/libcst-1.8.6-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6609291c41f7ad0bac570bfca5af8fea1f4a27987d30a1fa8b67fe5e67e6c78d", size = 2301289, upload-time = "2025-11-03T22:32:31.812Z" },
- { url = "https://files.pythonhosted.org/packages/5d/57/89f4ba7a6f1ac274eec9903a9e9174890d2198266eee8c00bc27eb45ecf7/libcst-1.8.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:25eaeae6567091443b5374b4c7d33a33636a2d58f5eda02135e96fc6c8807786", size = 2299230, upload-time = "2025-11-03T22:32:33.242Z" },
- { url = "https://files.pythonhosted.org/packages/f2/36/0aa693bc24cce163a942df49d36bf47a7ed614a0cd5598eee2623bc31913/libcst-1.8.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04030ea4d39d69a65873b1d4d877def1c3951a7ada1824242539e399b8763d30", size = 2408519, upload-time = "2025-11-03T22:32:34.678Z" },
- { url = "https://files.pythonhosted.org/packages/db/18/6dd055b5f15afa640fb3304b2ee9df8b7f72e79513814dbd0a78638f4a0e/libcst-1.8.6-cp313-cp313-win_amd64.whl", hash = "sha256:8066f1b70f21a2961e96bedf48649f27dfd5ea68be5cd1bed3742b047f14acde", size = 2119853, upload-time = "2025-11-03T22:32:36.287Z" },
- { url = "https://files.pythonhosted.org/packages/c9/ed/5ddb2a22f0b0abdd6dcffa40621ada1feaf252a15e5b2733a0a85dfd0429/libcst-1.8.6-cp313-cp313-win_arm64.whl", hash = "sha256:c188d06b583900e662cd791a3f962a8c96d3dfc9b36ea315be39e0a4c4792ebf", size = 1999808, upload-time = "2025-11-03T22:32:38.1Z" },
- { url = "https://files.pythonhosted.org/packages/25/d3/72b2de2c40b97e1ef4a1a1db4e5e52163fc7e7740ffef3846d30bc0096b5/libcst-1.8.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c41c76e034a1094afed7057023b1d8967f968782433f7299cd170eaa01ec033e", size = 2190553, upload-time = "2025-11-03T22:32:39.819Z" },
- { url = "https://files.pythonhosted.org/packages/0d/20/983b7b210ccc3ad94a82db54230e92599c4a11b9cfc7ce3bc97c1d2df75c/libcst-1.8.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5432e785322aba3170352f6e72b32bea58d28abd141ac37cc9b0bf6b7c778f58", size = 2074717, upload-time = "2025-11-03T22:32:41.373Z" },
- { url = "https://files.pythonhosted.org/packages/13/f2/9e01678fedc772e09672ed99930de7355757035780d65d59266fcee212b8/libcst-1.8.6-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:85b7025795b796dea5284d290ff69de5089fc8e989b25d6f6f15b6800be7167f", size = 2225834, upload-time = "2025-11-03T22:32:42.716Z" },
- { url = "https://files.pythonhosted.org/packages/4a/0d/7bed847b5c8c365e9f1953da274edc87577042bee5a5af21fba63276e756/libcst-1.8.6-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:536567441182a62fb706e7aa954aca034827b19746832205953b2c725d254a93", size = 2287107, upload-time = "2025-11-03T22:32:44.549Z" },
- { url = "https://files.pythonhosted.org/packages/02/f0/7e51fa84ade26c518bfbe7e2e4758b56d86a114c72d60309ac0d350426c4/libcst-1.8.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f04d3672bde1704f383a19e8f8331521abdbc1ed13abb349325a02ac56e5012", size = 2288672, upload-time = "2025-11-03T22:32:45.867Z" },
- { url = "https://files.pythonhosted.org/packages/ad/cd/15762659a3f5799d36aab1bc2b7e732672722e249d7800e3c5f943b41250/libcst-1.8.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f04febcd70e1e67917be7de513c8d4749d2e09206798558d7fe632134426ea4", size = 2392661, upload-time = "2025-11-03T22:32:47.232Z" },
- { url = "https://files.pythonhosted.org/packages/e4/6b/b7f9246c323910fcbe021241500f82e357521495dcfe419004dbb272c7cb/libcst-1.8.6-cp313-cp313t-win_amd64.whl", hash = "sha256:1dc3b897c8b0f7323412da3f4ad12b16b909150efc42238e19cbf19b561cc330", size = 2105068, upload-time = "2025-11-03T22:32:49.145Z" },
- { url = "https://files.pythonhosted.org/packages/a6/0b/4fd40607bc4807ec2b93b054594373d7fa3d31bb983789901afcb9bcebe9/libcst-1.8.6-cp313-cp313t-win_arm64.whl", hash = "sha256:44f38139fa95e488db0f8976f9c7ca39a64d6bc09f2eceef260aa1f6da6a2e42", size = 1985181, upload-time = "2025-11-03T22:32:50.597Z" },
- { url = "https://files.pythonhosted.org/packages/3a/60/4105441989e321f7ad0fd28ffccb83eb6aac0b7cfb0366dab855dcccfbe5/libcst-1.8.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b188e626ce61de5ad1f95161b8557beb39253de4ec74fc9b1f25593324a0279c", size = 2204202, upload-time = "2025-11-03T22:32:52.311Z" },
- { url = "https://files.pythonhosted.org/packages/67/2f/51a6f285c3a183e50cfe5269d4a533c21625aac2c8de5cdf2d41f079320d/libcst-1.8.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:87e74f7d7dfcba9efa91127081e22331d7c42515f0a0ac6e81d4cf2c3ed14661", size = 2083581, upload-time = "2025-11-03T22:32:54.269Z" },
- { url = "https://files.pythonhosted.org/packages/2f/64/921b1c19b638860af76cdb28bc81d430056592910b9478eea49e31a7f47a/libcst-1.8.6-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:3a926a4b42015ee24ddfc8ae940c97bd99483d286b315b3ce82f3bafd9f53474", size = 2236495, upload-time = "2025-11-03T22:32:55.723Z" },
- { url = "https://files.pythonhosted.org/packages/12/a8/b00592f9bede618cbb3df6ffe802fc65f1d1c03d48a10d353b108057d09c/libcst-1.8.6-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:3f4fbb7f569e69fd9e89d9d9caa57ca42c577c28ed05062f96a8c207594e75b8", size = 2301466, upload-time = "2025-11-03T22:32:57.337Z" },
- { url = "https://files.pythonhosted.org/packages/af/df/790d9002f31580fefd0aec2f373a0f5da99070e04c5e8b1c995d0104f303/libcst-1.8.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:08bd63a8ce674be431260649e70fca1d43f1554f1591eac657f403ff8ef82c7a", size = 2300264, upload-time = "2025-11-03T22:32:58.852Z" },
- { url = "https://files.pythonhosted.org/packages/21/de/dc3f10e65bab461be5de57850d2910a02c24c3ddb0da28f0e6e4133c3487/libcst-1.8.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e00e275d4ba95d4963431ea3e409aa407566a74ee2bf309a402f84fc744abe47", size = 2408572, upload-time = "2025-11-03T22:33:00.552Z" },
- { url = "https://files.pythonhosted.org/packages/20/3b/35645157a7590891038b077db170d6dd04335cd2e82a63bdaa78c3297dfe/libcst-1.8.6-cp314-cp314-win_amd64.whl", hash = "sha256:fea5c7fa26556eedf277d4f72779c5ede45ac3018650721edd77fd37ccd4a2d4", size = 2193917, upload-time = "2025-11-03T22:33:02.354Z" },
- { url = "https://files.pythonhosted.org/packages/b3/a2/1034a9ba7d3e82f2c2afaad84ba5180f601aed676d92b76325797ad60951/libcst-1.8.6-cp314-cp314-win_arm64.whl", hash = "sha256:bb9b4077bdf8857b2483879cbbf70f1073bc255b057ec5aac8a70d901bb838e9", size = 2078748, upload-time = "2025-11-03T22:33:03.707Z" },
- { url = "https://files.pythonhosted.org/packages/95/a1/30bc61e8719f721a5562f77695e6154e9092d1bdf467aa35d0806dcd6cea/libcst-1.8.6-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:55ec021a296960c92e5a33b8d93e8ad4182b0eab657021f45262510a58223de1", size = 2188980, upload-time = "2025-11-03T22:33:05.152Z" },
- { url = "https://files.pythonhosted.org/packages/2c/14/c660204532407c5628e3b615015a902ed2d0b884b77714a6bdbe73350910/libcst-1.8.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ba9ab2b012fbd53b36cafd8f4440a6b60e7e487cd8b87428e57336b7f38409a4", size = 2074828, upload-time = "2025-11-03T22:33:06.864Z" },
- { url = "https://files.pythonhosted.org/packages/82/e2/c497c354943dff644749f177ee9737b09ed811b8fc842b05709a40fe0d1b/libcst-1.8.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c0a0cc80aebd8aa15609dd4d330611cbc05e9b4216bcaeabba7189f99ef07c28", size = 2225568, upload-time = "2025-11-03T22:33:08.354Z" },
- { url = "https://files.pythonhosted.org/packages/86/ef/45999676d07bd6d0eefa28109b4f97124db114e92f9e108de42ba46a8028/libcst-1.8.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:42a4f68121e2e9c29f49c97f6154e8527cd31021809cc4a941c7270aa64f41aa", size = 2286523, upload-time = "2025-11-03T22:33:10.206Z" },
- { url = "https://files.pythonhosted.org/packages/f4/6c/517d8bf57d9f811862f4125358caaf8cd3320a01291b3af08f7b50719db4/libcst-1.8.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a434c521fadaf9680788b50d5c21f4048fa85ed19d7d70bd40549fbaeeecab1", size = 2288044, upload-time = "2025-11-03T22:33:11.628Z" },
- { url = "https://files.pythonhosted.org/packages/83/ce/24d7d49478ffb61207f229239879845da40a374965874f5ee60f96b02ddb/libcst-1.8.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6a65f844d813ab4ef351443badffa0ae358f98821561d19e18b3190f59e71996", size = 2392605, upload-time = "2025-11-03T22:33:12.962Z" },
- { url = "https://files.pythonhosted.org/packages/39/c3/829092ead738b71e96a4e96896c96f276976e5a8a58b4473ed813d7c962b/libcst-1.8.6-cp314-cp314t-win_amd64.whl", hash = "sha256:bdb14bc4d4d83a57062fed2c5da93ecb426ff65b0dc02ddf3481040f5f074a82", size = 2181581, upload-time = "2025-11-03T22:33:14.514Z" },
- { url = "https://files.pythonhosted.org/packages/98/6d/5d6a790a02eb0d9d36c4aed4f41b277497e6178900b2fa29c35353aa45ed/libcst-1.8.6-cp314-cp314t-win_arm64.whl", hash = "sha256:819c8081e2948635cab60c603e1bbdceccdfe19104a242530ad38a36222cb88f", size = 2065000, upload-time = "2025-11-03T22:33:16.257Z" },
-]
-
-[[package]]
-name = "loguru"
-version = "0.7.3"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
- { name = "win32-setctime", marker = "sys_platform == 'win32'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" },
-]
-
-[[package]]
-name = "markdown-it-py"
-version = "4.0.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "mdurl" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
-]
-
-[[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 = "mccabe"
-version = "0.7.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" },
-]
-
-[[package]]
-name = "mdurl"
-version = "0.1.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
-]
-
-[[package]]
-name = "more-itertools"
-version = "10.8.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" },
-]
-
-[[package]]
-name = "multidict"
-version = "6.7.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" },
- { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" },
- { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" },
- { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" },
- { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" },
- { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" },
- { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" },
- { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" },
- { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" },
- { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" },
- { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" },
- { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" },
- { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" },
- { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" },
- { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" },
- { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" },
- { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" },
- { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" },
- { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" },
- { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" },
- { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" },
- { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" },
- { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" },
- { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" },
- { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" },
- { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" },
- { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" },
- { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" },
- { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" },
- { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" },
- { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" },
- { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" },
- { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" },
- { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" },
- { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" },
- { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" },
- { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" },
- { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" },
- { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" },
- { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" },
- { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" },
- { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" },
- { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" },
- { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" },
- { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" },
- { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" },
- { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" },
- { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" },
- { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" },
- { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" },
- { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" },
- { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" },
- { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" },
- { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" },
- { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" },
- { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" },
- { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" },
- { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" },
- { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" },
- { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" },
- { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" },
- { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" },
- { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" },
- { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" },
- { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" },
- { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" },
- { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" },
- { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" },
- { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" },
- { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" },
- { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" },
- { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" },
- { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" },
- { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" },
- { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" },
- { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" },
- { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" },
- { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" },
- { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" },
- { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" },
- { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" },
- { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" },
- { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" },
- { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" },
- { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" },
- { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" },
- { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" },
- { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" },
- { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" },
- { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" },
- { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" },
- { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" },
- { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" },
- { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" },
- { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" },
- { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" },
- { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" },
- { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" },
- { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" },
- { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" },
- { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" },
- { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" },
- { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" },
- { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" },
- { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" },
- { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" },
- { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" },
- { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" },
- { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" },
-]
-
-[[package]]
-name = "mypy-extensions"
-version = "1.1.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
-]
-
-[[package]]
-name = "numpy"
-version = "2.4.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/24/62/ae72ff66c0f1fd959925b4c11f8c2dea61f47f6acaea75a08512cdfe3fed/numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690", size = 20721320, upload-time = "2026-01-10T06:44:59.619Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a5/34/2b1bc18424f3ad9af577f6ce23600319968a70575bd7db31ce66731bbef9/numpy-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0cce2a669e3c8ba02ee563c7835f92c153cf02edff1ae05e1823f1dde21b16a5", size = 16944563, upload-time = "2026-01-10T06:42:14.615Z" },
- { url = "https://files.pythonhosted.org/packages/2c/57/26e5f97d075aef3794045a6ca9eada6a4ed70eb9a40e7a4a93f9ac80d704/numpy-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:899d2c18024984814ac7e83f8f49d8e8180e2fbe1b2e252f2e7f1d06bea92425", size = 12645658, upload-time = "2026-01-10T06:42:17.298Z" },
- { url = "https://files.pythonhosted.org/packages/8e/ba/80fc0b1e3cb2fd5c6143f00f42eb67762aa043eaa05ca924ecc3222a7849/numpy-2.4.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:09aa8a87e45b55a1c2c205d42e2808849ece5c484b2aab11fecabec3841cafba", size = 5474132, upload-time = "2026-01-10T06:42:19.637Z" },
- { url = "https://files.pythonhosted.org/packages/40/ae/0a5b9a397f0e865ec171187c78d9b57e5588afc439a04ba9cab1ebb2c945/numpy-2.4.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:edee228f76ee2dab4579fad6f51f6a305de09d444280109e0f75df247ff21501", size = 6804159, upload-time = "2026-01-10T06:42:21.44Z" },
- { url = "https://files.pythonhosted.org/packages/86/9c/841c15e691c7085caa6fd162f063eff494099c8327aeccd509d1ab1e36ab/numpy-2.4.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a92f227dbcdc9e4c3e193add1a189a9909947d4f8504c576f4a732fd0b54240a", size = 14708058, upload-time = "2026-01-10T06:42:23.546Z" },
- { url = "https://files.pythonhosted.org/packages/5d/9d/7862db06743f489e6a502a3b93136d73aea27d97b2cf91504f70a27501d6/numpy-2.4.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:538bf4ec353709c765ff75ae616c34d3c3dca1a68312727e8f2676ea644f8509", size = 16651501, upload-time = "2026-01-10T06:42:25.909Z" },
- { url = "https://files.pythonhosted.org/packages/a6/9c/6fc34ebcbd4015c6e5f0c0ce38264010ce8a546cb6beacb457b84a75dfc8/numpy-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ac08c63cb7779b85e9d5318e6c3518b424bc1f364ac4cb2c6136f12e5ff2dccc", size = 16492627, upload-time = "2026-01-10T06:42:28.938Z" },
- { url = "https://files.pythonhosted.org/packages/aa/63/2494a8597502dacda439f61b3c0db4da59928150e62be0e99395c3ad23c5/numpy-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f9c360ecef085e5841c539a9a12b883dff005fbd7ce46722f5e9cef52634d82", size = 18585052, upload-time = "2026-01-10T06:42:31.312Z" },
- { url = "https://files.pythonhosted.org/packages/6a/93/098e1162ae7522fc9b618d6272b77404c4656c72432ecee3abc029aa3de0/numpy-2.4.1-cp311-cp311-win32.whl", hash = "sha256:0f118ce6b972080ba0758c6087c3617b5ba243d806268623dc34216d69099ba0", size = 6236575, upload-time = "2026-01-10T06:42:33.872Z" },
- { url = "https://files.pythonhosted.org/packages/8c/de/f5e79650d23d9e12f38a7bc6b03ea0835b9575494f8ec94c11c6e773b1b1/numpy-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:18e14c4d09d55eef39a6ab5b08406e84bc6869c1e34eef45564804f90b7e0574", size = 12604479, upload-time = "2026-01-10T06:42:35.778Z" },
- { url = "https://files.pythonhosted.org/packages/dd/65/e1097a7047cff12ce3369bd003811516b20ba1078dbdec135e1cd7c16c56/numpy-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:6461de5113088b399d655d45c3897fa188766415d0f568f175ab071c8873bd73", size = 10578325, upload-time = "2026-01-10T06:42:38.518Z" },
- { url = "https://files.pythonhosted.org/packages/78/7f/ec53e32bf10c813604edf07a3682616bd931d026fcde7b6d13195dfb684a/numpy-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2", size = 16656888, upload-time = "2026-01-10T06:42:40.913Z" },
- { url = "https://files.pythonhosted.org/packages/b8/e0/1f9585d7dae8f14864e948fd7fa86c6cb72dee2676ca2748e63b1c5acfe0/numpy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8", size = 12373956, upload-time = "2026-01-10T06:42:43.091Z" },
- { url = "https://files.pythonhosted.org/packages/8e/43/9762e88909ff2326f5e7536fa8cb3c49fb03a7d92705f23e6e7f553d9cb3/numpy-2.4.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a", size = 5202567, upload-time = "2026-01-10T06:42:45.107Z" },
- { url = "https://files.pythonhosted.org/packages/4b/ee/34b7930eb61e79feb4478800a4b95b46566969d837546aa7c034c742ef98/numpy-2.4.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0", size = 6549459, upload-time = "2026-01-10T06:42:48.152Z" },
- { url = "https://files.pythonhosted.org/packages/79/e3/5f115fae982565771be994867c89bcd8d7208dbfe9469185497d70de5ddf/numpy-2.4.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c", size = 14404859, upload-time = "2026-01-10T06:42:49.947Z" },
- { url = "https://files.pythonhosted.org/packages/d9/7d/9c8a781c88933725445a859cac5d01b5871588a15969ee6aeb618ba99eee/numpy-2.4.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02", size = 16371419, upload-time = "2026-01-10T06:42:52.409Z" },
- { url = "https://files.pythonhosted.org/packages/a6/d2/8aa084818554543f17cf4162c42f162acbd3bb42688aefdba6628a859f77/numpy-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162", size = 16182131, upload-time = "2026-01-10T06:42:54.694Z" },
- { url = "https://files.pythonhosted.org/packages/60/db/0425216684297c58a8df35f3284ef56ec4a043e6d283f8a59c53562caf1b/numpy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9", size = 18295342, upload-time = "2026-01-10T06:42:56.991Z" },
- { url = "https://files.pythonhosted.org/packages/31/4c/14cb9d86240bd8c386c881bafbe43f001284b7cce3bc01623ac9475da163/numpy-2.4.1-cp312-cp312-win32.whl", hash = "sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f", size = 5959015, upload-time = "2026-01-10T06:42:59.631Z" },
- { url = "https://files.pythonhosted.org/packages/51/cf/52a703dbeb0c65807540d29699fef5fda073434ff61846a564d5c296420f/numpy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87", size = 12310730, upload-time = "2026-01-10T06:43:01.627Z" },
- { url = "https://files.pythonhosted.org/packages/69/80/a828b2d0ade5e74a9fe0f4e0a17c30fdc26232ad2bc8c9f8b3197cf7cf18/numpy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8", size = 10312166, upload-time = "2026-01-10T06:43:03.673Z" },
- { url = "https://files.pythonhosted.org/packages/04/68/732d4b7811c00775f3bd522a21e8dd5a23f77eb11acdeb663e4a4ebf0ef4/numpy-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d797454e37570cfd61143b73b8debd623c3c0952959adb817dd310a483d58a1b", size = 16652495, upload-time = "2026-01-10T06:43:06.283Z" },
- { url = "https://files.pythonhosted.org/packages/20/ca/857722353421a27f1465652b2c66813eeeccea9d76d5f7b74b99f298e60e/numpy-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c55962006156aeef1629b953fd359064aa47e4d82cfc8e67f0918f7da3344f", size = 12368657, upload-time = "2026-01-10T06:43:09.094Z" },
- { url = "https://files.pythonhosted.org/packages/81/0d/2377c917513449cc6240031a79d30eb9a163d32a91e79e0da47c43f2c0c8/numpy-2.4.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:71abbea030f2cfc3092a0ff9f8c8fdefdc5e0bf7d9d9c99663538bb0ecdac0b9", size = 5197256, upload-time = "2026-01-10T06:43:13.634Z" },
- { url = "https://files.pythonhosted.org/packages/17/39/569452228de3f5de9064ac75137082c6214be1f5c532016549a7923ab4b5/numpy-2.4.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b55aa56165b17aaf15520beb9cbd33c9039810e0d9643dd4379e44294c7303e", size = 6545212, upload-time = "2026-01-10T06:43:15.661Z" },
- { url = "https://files.pythonhosted.org/packages/8c/a4/77333f4d1e4dac4395385482557aeecf4826e6ff517e32ca48e1dafbe42a/numpy-2.4.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0faba4a331195bfa96f93dd9dfaa10b2c7aa8cda3a02b7fd635e588fe821bf5", size = 14402871, upload-time = "2026-01-10T06:43:17.324Z" },
- { url = "https://files.pythonhosted.org/packages/ba/87/d341e519956273b39d8d47969dd1eaa1af740615394fe67d06f1efa68773/numpy-2.4.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e3087f53e2b4428766b54932644d148613c5a595150533ae7f00dab2f319a8", size = 16359305, upload-time = "2026-01-10T06:43:19.376Z" },
- { url = "https://files.pythonhosted.org/packages/32/91/789132c6666288eaa20ae8066bb99eba1939362e8f1a534949a215246e97/numpy-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:49e792ec351315e16da54b543db06ca8a86985ab682602d90c60ef4ff4db2a9c", size = 16181909, upload-time = "2026-01-10T06:43:21.808Z" },
- { url = "https://files.pythonhosted.org/packages/cf/b8/090b8bd27b82a844bb22ff8fdf7935cb1980b48d6e439ae116f53cdc2143/numpy-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79e9e06c4c2379db47f3f6fc7a8652e7498251789bf8ff5bd43bf478ef314ca2", size = 18284380, upload-time = "2026-01-10T06:43:23.957Z" },
- { url = "https://files.pythonhosted.org/packages/67/78/722b62bd31842ff029412271556a1a27a98f45359dea78b1548a3a9996aa/numpy-2.4.1-cp313-cp313-win32.whl", hash = "sha256:3d1a100e48cb266090a031397863ff8a30050ceefd798f686ff92c67a486753d", size = 5957089, upload-time = "2026-01-10T06:43:27.535Z" },
- { url = "https://files.pythonhosted.org/packages/da/a6/cf32198b0b6e18d4fbfa9a21a992a7fca535b9bb2b0cdd217d4a3445b5ca/numpy-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:92a0e65272fd60bfa0d9278e0484c2f52fe03b97aedc02b357f33fe752c52ffb", size = 12307230, upload-time = "2026-01-10T06:43:29.298Z" },
- { url = "https://files.pythonhosted.org/packages/44/6c/534d692bfb7d0afe30611320c5fb713659dcb5104d7cc182aff2aea092f5/numpy-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:20d4649c773f66cc2fc36f663e091f57c3b7655f936a4c681b4250855d1da8f5", size = 10313125, upload-time = "2026-01-10T06:43:31.782Z" },
- { url = "https://files.pythonhosted.org/packages/da/a1/354583ac5c4caa566de6ddfbc42744409b515039e085fab6e0ff942e0df5/numpy-2.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f93bc6892fe7b0663e5ffa83b61aab510aacffd58c16e012bb9352d489d90cb7", size = 12496156, upload-time = "2026-01-10T06:43:34.237Z" },
- { url = "https://files.pythonhosted.org/packages/51/b0/42807c6e8cce58c00127b1dc24d365305189991f2a7917aa694a109c8d7d/numpy-2.4.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:178de8f87948163d98a4c9ab5bee4ce6519ca918926ec8df195af582de28544d", size = 5324663, upload-time = "2026-01-10T06:43:36.211Z" },
- { url = "https://files.pythonhosted.org/packages/fe/55/7a621694010d92375ed82f312b2f28017694ed784775269115323e37f5e2/numpy-2.4.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:98b35775e03ab7f868908b524fc0a84d38932d8daf7b7e1c3c3a1b6c7a2c9f15", size = 6645224, upload-time = "2026-01-10T06:43:37.884Z" },
- { url = "https://files.pythonhosted.org/packages/50/96/9fa8635ed9d7c847d87e30c834f7109fac5e88549d79ef3324ab5c20919f/numpy-2.4.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941c2a93313d030f219f3a71fd3d91a728b82979a5e8034eb2e60d394a2b83f9", size = 14462352, upload-time = "2026-01-10T06:43:39.479Z" },
- { url = "https://files.pythonhosted.org/packages/03/d1/8cf62d8bb2062da4fb82dd5d49e47c923f9c0738032f054e0a75342faba7/numpy-2.4.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:529050522e983e00a6c1c6b67411083630de8b57f65e853d7b03d9281b8694d2", size = 16407279, upload-time = "2026-01-10T06:43:41.93Z" },
- { url = "https://files.pythonhosted.org/packages/86/1c/95c86e17c6b0b31ce6ef219da00f71113b220bcb14938c8d9a05cee0ff53/numpy-2.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2302dc0224c1cbc49bb94f7064f3f923a971bfae45c33870dcbff63a2a550505", size = 16248316, upload-time = "2026-01-10T06:43:44.121Z" },
- { url = "https://files.pythonhosted.org/packages/30/b4/e7f5ff8697274c9d0fa82398b6a372a27e5cef069b37df6355ccb1f1db1a/numpy-2.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9171a42fcad32dcf3fa86f0a4faa5e9f8facefdb276f54b8b390d90447cff4e2", size = 18329884, upload-time = "2026-01-10T06:43:46.613Z" },
- { url = "https://files.pythonhosted.org/packages/37/a4/b073f3e9d77f9aec8debe8ca7f9f6a09e888ad1ba7488f0c3b36a94c03ac/numpy-2.4.1-cp313-cp313t-win32.whl", hash = "sha256:382ad67d99ef49024f11d1ce5dcb5ad8432446e4246a4b014418ba3a1175a1f4", size = 6081138, upload-time = "2026-01-10T06:43:48.854Z" },
- { url = "https://files.pythonhosted.org/packages/16/16/af42337b53844e67752a092481ab869c0523bc95c4e5c98e4dac4e9581ac/numpy-2.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:62fea415f83ad8fdb6c20840578e5fbaf5ddd65e0ec6c3c47eda0f69da172510", size = 12447478, upload-time = "2026-01-10T06:43:50.476Z" },
- { url = "https://files.pythonhosted.org/packages/6c/f8/fa85b2eac68ec631d0b631abc448552cb17d39afd17ec53dcbcc3537681a/numpy-2.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a7870e8c5fc11aef57d6fea4b4085e537a3a60ad2cdd14322ed531fdca68d261", size = 10382981, upload-time = "2026-01-10T06:43:52.575Z" },
- { url = "https://files.pythonhosted.org/packages/1b/a7/ef08d25698e0e4b4efbad8d55251d20fe2a15f6d9aa7c9b30cd03c165e6f/numpy-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3869ea1ee1a1edc16c29bbe3a2f2a4e515cc3a44d43903ad41e0cacdbaf733dc", size = 16652046, upload-time = "2026-01-10T06:43:54.797Z" },
- { url = "https://files.pythonhosted.org/packages/8f/39/e378b3e3ca13477e5ac70293ec027c438d1927f18637e396fe90b1addd72/numpy-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e867df947d427cdd7a60e3e271729090b0f0df80f5f10ab7dd436f40811699c3", size = 12378858, upload-time = "2026-01-10T06:43:57.099Z" },
- { url = "https://files.pythonhosted.org/packages/c3/74/7ec6154f0006910ed1fdbb7591cf4432307033102b8a22041599935f8969/numpy-2.4.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e3bd2cb07841166420d2fa7146c96ce00cb3410664cbc1a6be028e456c4ee220", size = 5207417, upload-time = "2026-01-10T06:43:59.037Z" },
- { url = "https://files.pythonhosted.org/packages/f7/b7/053ac11820d84e42f8feea5cb81cc4fcd1091499b45b1ed8c7415b1bf831/numpy-2.4.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:f0a90aba7d521e6954670550e561a4cb925713bd944445dbe9e729b71f6cabee", size = 6542643, upload-time = "2026-01-10T06:44:01.852Z" },
- { url = "https://files.pythonhosted.org/packages/c0/c4/2e7908915c0e32ca636b92e4e4a3bdec4cb1e7eb0f8aedf1ed3c68a0d8cd/numpy-2.4.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d558123217a83b2d1ba316b986e9248a1ed1971ad495963d555ccd75dcb1556", size = 14418963, upload-time = "2026-01-10T06:44:04.047Z" },
- { url = "https://files.pythonhosted.org/packages/eb/c0/3ed5083d94e7ffd7c404e54619c088e11f2e1939a9544f5397f4adb1b8ba/numpy-2.4.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f44de05659b67d20499cbc96d49f2650769afcb398b79b324bb6e297bfe3844", size = 16363811, upload-time = "2026-01-10T06:44:06.207Z" },
- { url = "https://files.pythonhosted.org/packages/0e/68/42b66f1852bf525050a67315a4fb94586ab7e9eaa541b1bef530fab0c5dd/numpy-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:69e7419c9012c4aaf695109564e3387f1259f001b4326dfa55907b098af082d3", size = 16197643, upload-time = "2026-01-10T06:44:08.33Z" },
- { url = "https://files.pythonhosted.org/packages/d2/40/e8714fc933d85f82c6bfc7b998a0649ad9769a32f3494ba86598aaf18a48/numpy-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffd257026eb1b34352e749d7cc1678b5eeec3e329ad8c9965a797e08ccba205", size = 18289601, upload-time = "2026-01-10T06:44:10.841Z" },
- { url = "https://files.pythonhosted.org/packages/80/9a/0d44b468cad50315127e884802351723daca7cf1c98d102929468c81d439/numpy-2.4.1-cp314-cp314-win32.whl", hash = "sha256:727c6c3275ddefa0dc078524a85e064c057b4f4e71ca5ca29a19163c607be745", size = 6005722, upload-time = "2026-01-10T06:44:13.332Z" },
- { url = "https://files.pythonhosted.org/packages/7e/bb/c6513edcce5a831810e2dddc0d3452ce84d208af92405a0c2e58fd8e7881/numpy-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:7d5d7999df434a038d75a748275cd6c0094b0ecdb0837342b332a82defc4dc4d", size = 12438590, upload-time = "2026-01-10T06:44:15.006Z" },
- { url = "https://files.pythonhosted.org/packages/e9/da/a598d5cb260780cf4d255102deba35c1d072dc028c4547832f45dd3323a8/numpy-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:ce9ce141a505053b3c7bce3216071f3bf5c182b8b28930f14cd24d43932cd2df", size = 10596180, upload-time = "2026-01-10T06:44:17.386Z" },
- { url = "https://files.pythonhosted.org/packages/de/bc/ea3f2c96fcb382311827231f911723aeff596364eb6e1b6d1d91128aa29b/numpy-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e53170557d37ae404bf8d542ca5b7c629d6efa1117dac6a83e394142ea0a43f", size = 12498774, upload-time = "2026-01-10T06:44:19.467Z" },
- { url = "https://files.pythonhosted.org/packages/aa/ab/ef9d939fe4a812648c7a712610b2ca6140b0853c5efea361301006c02ae5/numpy-2.4.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:a73044b752f5d34d4232f25f18160a1cc418ea4507f5f11e299d8ac36875f8a0", size = 5327274, upload-time = "2026-01-10T06:44:23.189Z" },
- { url = "https://files.pythonhosted.org/packages/bd/31/d381368e2a95c3b08b8cf7faac6004849e960f4a042d920337f71cef0cae/numpy-2.4.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:fb1461c99de4d040666ca0444057b06541e5642f800b71c56e6ea92d6a853a0c", size = 6648306, upload-time = "2026-01-10T06:44:25.012Z" },
- { url = "https://files.pythonhosted.org/packages/c8/e5/0989b44ade47430be6323d05c23207636d67d7362a1796ccbccac6773dd2/numpy-2.4.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423797bdab2eeefbe608d7c1ec7b2b4fd3c58d51460f1ee26c7500a1d9c9ee93", size = 14464653, upload-time = "2026-01-10T06:44:26.706Z" },
- { url = "https://files.pythonhosted.org/packages/10/a7/cfbe475c35371cae1358e61f20c5f075badc18c4797ab4354140e1d283cf/numpy-2.4.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52b5f61bdb323b566b528899cc7db2ba5d1015bda7ea811a8bcf3c89c331fa42", size = 16405144, upload-time = "2026-01-10T06:44:29.378Z" },
- { url = "https://files.pythonhosted.org/packages/f8/a3/0c63fe66b534888fa5177cc7cef061541064dbe2b4b60dcc60ffaf0d2157/numpy-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42d7dd5fa36d16d52a84f821eb96031836fd405ee6955dd732f2023724d0aa01", size = 16247425, upload-time = "2026-01-10T06:44:31.721Z" },
- { url = "https://files.pythonhosted.org/packages/6b/2b/55d980cfa2c93bd40ff4c290bf824d792bd41d2fe3487b07707559071760/numpy-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7b6b5e28bbd47b7532698e5db2fe1db693d84b58c254e4389d99a27bb9b8f6b", size = 18330053, upload-time = "2026-01-10T06:44:34.617Z" },
- { url = "https://files.pythonhosted.org/packages/23/12/8b5fc6b9c487a09a7957188e0943c9ff08432c65e34567cabc1623b03a51/numpy-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:5de60946f14ebe15e713a6f22850c2372fa72f4ff9a432ab44aa90edcadaa65a", size = 6152482, upload-time = "2026-01-10T06:44:36.798Z" },
- { url = "https://files.pythonhosted.org/packages/00/a5/9f8ca5856b8940492fc24fbe13c1bc34d65ddf4079097cf9e53164d094e1/numpy-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f085da926c0d491ffff3096f91078cc97ea67e7e6b65e490bc8dcda65663be2", size = 12627117, upload-time = "2026-01-10T06:44:38.828Z" },
- { url = "https://files.pythonhosted.org/packages/ad/0d/eca3d962f9eef265f01a8e0d20085c6dd1f443cbffc11b6dede81fd82356/numpy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295", size = 10667121, upload-time = "2026-01-10T06:44:41.644Z" },
- { url = "https://files.pythonhosted.org/packages/1e/48/d86f97919e79314a1cdee4c832178763e6e98e623e123d0bada19e92c15a/numpy-2.4.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ad35f20be147a204e28b6a0575fbf3540c5e5f802634d4258d55b1ff5facce1", size = 16822202, upload-time = "2026-01-10T06:44:43.738Z" },
- { url = "https://files.pythonhosted.org/packages/51/e9/1e62a7f77e0f37dcfb0ad6a9744e65df00242b6ea37dfafb55debcbf5b55/numpy-2.4.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8097529164c0f3e32bb89412a0905d9100bf434d9692d9fc275e18dcf53c9344", size = 12569985, upload-time = "2026-01-10T06:44:45.945Z" },
- { url = "https://files.pythonhosted.org/packages/c7/7e/914d54f0c801342306fdcdce3e994a56476f1b818c46c47fc21ae968088c/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:ea66d2b41ca4a1630aae5507ee0a71647d3124d1741980138aa8f28f44dac36e", size = 5398484, upload-time = "2026-01-10T06:44:48.012Z" },
- { url = "https://files.pythonhosted.org/packages/1c/d8/9570b68584e293a33474e7b5a77ca404f1dcc655e40050a600dee81d27fb/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d3f8f0df9f4b8be57b3bf74a1d087fec68f927a2fab68231fdb442bf2c12e426", size = 6713216, upload-time = "2026-01-10T06:44:49.725Z" },
- { url = "https://files.pythonhosted.org/packages/33/9b/9dd6e2db8d49eb24f86acaaa5258e5f4c8ed38209a4ee9de2d1a0ca25045/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2023ef86243690c2791fd6353e5b4848eedaa88ca8a2d129f462049f6d484696", size = 14538937, upload-time = "2026-01-10T06:44:51.498Z" },
- { url = "https://files.pythonhosted.org/packages/53/87/d5bd995b0f798a37105b876350d346eea5838bd8f77ea3d7a48392f3812b/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8361ea4220d763e54cff2fbe7d8c93526b744f7cd9ddab47afeff7e14e8503be", size = 16479830, upload-time = "2026-01-10T06:44:53.931Z" },
- { url = "https://files.pythonhosted.org/packages/5b/c7/b801bf98514b6ae6475e941ac05c58e6411dd863ea92916bfd6d510b08c1/numpy-2.4.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4f1b68ff47680c2925f8063402a693ede215f0257f02596b1318ecdfb1d79e33", size = 12492579, upload-time = "2026-01-10T06:44:57.094Z" },
-]
-
-[[package]]
-name = "openai"
-version = "2.16.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/b1/6c/e4c964fcf1d527fdf4739e7cc940c60075a4114d50d03871d5d5b1e13a88/openai-2.16.0.tar.gz", hash = "sha256:42eaa22ca0d8ded4367a77374104d7a2feafee5bd60a107c3c11b5243a11cd12", size = 629649, upload-time = "2026-01-27T23:28:02.579Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/16/83/0315bf2cfd75a2ce8a7e54188e9456c60cec6c0cf66728ed07bd9859ff26/openai-2.16.0-py3-none-any.whl", hash = "sha256:5f46643a8f42899a84e80c38838135d7038e7718333ce61396994f887b09a59b", size = 1068612, upload-time = "2026-01-27T23:28:00.356Z" },
-]
-
-[[package]]
-name = "packaging"
-version = "26.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
-]
-
-[[package]]
-name = "pathspec"
-version = "1.0.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" },
-]
-
-[[package]]
-name = "pillow"
-version = "11.3.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" },
- { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" },
- { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" },
- { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" },
- { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" },
- { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" },
- { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" },
- { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" },
- { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" },
- { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" },
- { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" },
- { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" },
- { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" },
- { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" },
- { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" },
- { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" },
- { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" },
- { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" },
- { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" },
- { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" },
- { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" },
- { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" },
- { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" },
- { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" },
- { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" },
- { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" },
- { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" },
- { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" },
- { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" },
- { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" },
- { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" },
- { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" },
- { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" },
- { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" },
- { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" },
- { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" },
- { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" },
- { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" },
- { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" },
- { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" },
- { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" },
- { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" },
- { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" },
- { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" },
- { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" },
- { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" },
- { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" },
- { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" },
- { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" },
- { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" },
- { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" },
- { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" },
- { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" },
- { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" },
- { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" },
- { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" },
- { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" },
- { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" },
- { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" },
- { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" },
- { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" },
- { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" },
- { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" },
- { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" },
- { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" },
- { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" },
- { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" },
- { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" },
- { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" },
- { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" },
- { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" },
- { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" },
- { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" },
- { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" },
- { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" },
- { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" },
-]
-
-[[package]]
-name = "platformdirs"
-version = "4.5.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
-]
-
-[[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 = "prometheus-client"
-version = "0.24.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" },
-]
-
-[[package]]
-name = "propcache"
-version = "0.4.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" },
- { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" },
- { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" },
- { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" },
- { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" },
- { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" },
- { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" },
- { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" },
- { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" },
- { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" },
- { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" },
- { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" },
- { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" },
- { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" },
- { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" },
- { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" },
- { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" },
- { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" },
- { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" },
- { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" },
- { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" },
- { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" },
- { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" },
- { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" },
- { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" },
- { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" },
- { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" },
- { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" },
- { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" },
- { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" },
- { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" },
- { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" },
- { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" },
- { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" },
- { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" },
- { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" },
- { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" },
- { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" },
- { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" },
- { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" },
- { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" },
- { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" },
- { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" },
- { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" },
- { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" },
- { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" },
- { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" },
- { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" },
- { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" },
- { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" },
- { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" },
- { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" },
- { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" },
- { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" },
- { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" },
- { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" },
- { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" },
- { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" },
- { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" },
- { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" },
- { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" },
- { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" },
- { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" },
- { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" },
- { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" },
- { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" },
- { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" },
- { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" },
- { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" },
- { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" },
- { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" },
- { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" },
- { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" },
- { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" },
- { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" },
- { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" },
- { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" },
- { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" },
- { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" },
- { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" },
- { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" },
- { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" },
- { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" },
- { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" },
- { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" },
- { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" },
- { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" },
- { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" },
- { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" },
- { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" },
- { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" },
-]
-
-[[package]]
-name = "psycopg"
-version = "3.3.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "typing-extensions", marker = "python_full_version < '3.13'" },
- { name = "tzdata", marker = "sys_platform == 'win32'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/e0/1a/7d9ef4fdc13ef7f15b934c393edc97a35c281bb7d3c3329fbfcbe915a7c2/psycopg-3.3.2.tar.gz", hash = "sha256:707a67975ee214d200511177a6a80e56e654754c9afca06a7194ea6bbfde9ca7", size = 165630, upload-time = "2025-12-06T17:34:53.899Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/8c/51/2779ccdf9305981a06b21a6b27e8547c948d85c41c76ff434192784a4c93/psycopg-3.3.2-py3-none-any.whl", hash = "sha256:3e94bc5f4690247d734599af56e51bae8e0db8e4311ea413f801fef82b14a99b", size = 212774, upload-time = "2025-12-06T17:31:41.414Z" },
-]
-
-[package.optional-dependencies]
-binary = [
- { name = "psycopg-binary", marker = "implementation_name != 'pypy'" },
-]
-
-[[package]]
-name = "psycopg-binary"
-version = "3.3.2"
-source = { registry = "https://pypi.org/simple" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/57/d9/49640360fc090d27afc4655021544aa71d5393ebae124ffa53a04474b493/psycopg_binary-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:94503b79f7da0b65c80d0dbb2f81dd78b300319ec2435d5e6dcf9622160bc2fa", size = 4597890, upload-time = "2025-12-06T17:32:23.087Z" },
- { url = "https://files.pythonhosted.org/packages/85/cf/99634bbccc8af0dd86df4bce705eea5540d06bb7f5ab3067446ae9ffdae4/psycopg_binary-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07a5f030e0902ec3e27d0506ceb01238c0aecbc73ecd7fa0ee55f86134600b5b", size = 4664396, upload-time = "2025-12-06T17:32:26.421Z" },
- { url = "https://files.pythonhosted.org/packages/40/db/6035dff6d5c6dfca3a4ab0d2ac62ede623646e327e9f99e21e0cf08976c6/psycopg_binary-3.3.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e09d0d93d35c134704a2cb2b15f81ffc8174fd602f3e08f7b1a3d8896156cf0", size = 5478743, upload-time = "2025-12-06T17:32:29.901Z" },
- { url = "https://files.pythonhosted.org/packages/03/0f/fc06bbc8e87f09458d2ce04a59cd90565e54e8efca33e0802daee6d2b0e6/psycopg_binary-3.3.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:649c1d33bedda431e0c1df646985fbbeb9274afa964e1aef4be053c0f23a2924", size = 5151820, upload-time = "2025-12-06T17:32:33.562Z" },
- { url = "https://files.pythonhosted.org/packages/86/ab/bcc0397c96a0ad29463e33ed03285826e0fabc43595c195f419d9291ee70/psycopg_binary-3.3.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5774272f754605059521ff037a86e680342e3847498b0aa86b0f3560c70963c", size = 6747711, upload-time = "2025-12-06T17:32:38.074Z" },
- { url = "https://files.pythonhosted.org/packages/96/eb/7450bc75c31d5be5f7a6d02d26beef6989a4ca6f5efdec65eea6cf612d0e/psycopg_binary-3.3.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d391b70c9cc23f6e1142729772a011f364199d2c5ddc0d596f5f43316fbf982d", size = 4991626, upload-time = "2025-12-06T17:32:41.373Z" },
- { url = "https://files.pythonhosted.org/packages/dc/85/65f14453804c82a7fba31cd1a984b90349c0f327b809102c4b99115c0930/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f3f601f32244a677c7b029ec39412db2772ad04a28bc2cbb4b1f0931ed0ffad7", size = 4516760, upload-time = "2025-12-06T17:32:44.921Z" },
- { url = "https://files.pythonhosted.org/packages/24/8c/3105f00a91d73d9a443932f95156eae8159d5d9cb68a9d2cf512710d484f/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0ae60e910531cfcc364a8f615a7941cac89efeb3f0fffe0c4824a6d11461eef7", size = 4204028, upload-time = "2025-12-06T17:32:48.355Z" },
- { url = "https://files.pythonhosted.org/packages/1e/dd/74f64a383342ef7c22d1eb2768ed86411c7f877ed2580cd33c17f436fe3c/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c43a773dd1a481dbb2fe64576aa303d80f328cce0eae5e3e4894947c41d1da7", size = 3935780, upload-time = "2025-12-06T17:32:51.347Z" },
- { url = "https://files.pythonhosted.org/packages/85/30/f3f207d1c292949a26cdea6727c9c325b4ee41e04bf2736a4afbe45eb61f/psycopg_binary-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5a327327f1188b3fbecac41bf1973a60b86b2eb237db10dc945bd3dc97ec39e4", size = 4243239, upload-time = "2025-12-06T17:32:54.924Z" },
- { url = "https://files.pythonhosted.org/packages/b3/08/8f1b5d6231338bf7bc46f635c4d4965facec52e1c9a7952ca8a70cb57dc0/psycopg_binary-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:136c43f185244893a527540307167f5d3ef4e08786508afe45d6f146228f5aa9", size = 3548102, upload-time = "2025-12-06T17:32:57.944Z" },
- { url = "https://files.pythonhosted.org/packages/4e/1e/8614b01c549dd7e385dacdcd83fe194f6b3acb255a53cc67154ee6bf00e7/psycopg_binary-3.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9387ab615f929e71ef0f4a8a51e986fa06236ccfa9f3ec98a88f60fbf230634", size = 4579832, upload-time = "2025-12-06T17:33:01.388Z" },
- { url = "https://files.pythonhosted.org/packages/26/97/0bb093570fae2f4454d42c1ae6000f15934391867402f680254e4a7def54/psycopg_binary-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3ff7489df5e06c12d1829544eaec64970fe27fe300f7cf04c8495fe682064688", size = 4658786, upload-time = "2025-12-06T17:33:05.022Z" },
- { url = "https://files.pythonhosted.org/packages/61/20/1d9383e3f2038826900a14137b0647d755f67551aab316e1021443105ed5/psycopg_binary-3.3.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:9742580ecc8e1ac45164e98d32ca6df90da509c2d3ff26be245d94c430f92db4", size = 5454896, upload-time = "2025-12-06T17:33:09.023Z" },
- { url = "https://files.pythonhosted.org/packages/a6/62/513c80ad8bbb545e364f7737bf2492d34a4c05eef4f7b5c16428dc42260d/psycopg_binary-3.3.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d45acedcaa58619355f18e0f42af542fcad3fd84ace4b8355d3a5dea23318578", size = 5132731, upload-time = "2025-12-06T17:33:12.519Z" },
- { url = "https://files.pythonhosted.org/packages/f3/28/ddf5f5905f088024bccb19857949467407c693389a14feb527d6171d8215/psycopg_binary-3.3.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d88f32ff8c47cb7f4e7e7a9d1747dcee6f3baa19ed9afa9e5694fd2fb32b61ed", size = 6724495, upload-time = "2025-12-06T17:33:16.624Z" },
- { url = "https://files.pythonhosted.org/packages/6e/93/a1157ebcc650960b264542b547f7914d87a42ff0cc15a7584b29d5807e6b/psycopg_binary-3.3.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:59d0163c4617a2c577cb34afbed93d7a45b8c8364e54b2bd2020ff25d5f5f860", size = 4964979, upload-time = "2025-12-06T17:33:20.179Z" },
- { url = "https://files.pythonhosted.org/packages/0e/27/65939ba6798f9c5be4a5d9cd2061ebaf0851798525c6811d347821c8132d/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e750afe74e6c17b2c7046d2c3e3173b5a3f6080084671c8aa327215323df155b", size = 4493648, upload-time = "2025-12-06T17:33:23.464Z" },
- { url = "https://files.pythonhosted.org/packages/8a/c4/5e9e4b9b1c1e27026e43387b0ba4aaf3537c7806465dd3f1d5bde631752a/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f26f113013c4dcfbfe9ced57b5bad2035dda1a7349f64bf726021968f9bccad3", size = 4173392, upload-time = "2025-12-06T17:33:26.88Z" },
- { url = "https://files.pythonhosted.org/packages/c6/81/cf43fb76993190cee9af1cbcfe28afb47b1928bdf45a252001017e5af26e/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8309ee4569dced5e81df5aa2dcd48c7340c8dee603a66430f042dfbd2878edca", size = 3909241, upload-time = "2025-12-06T17:33:30.092Z" },
- { url = "https://files.pythonhosted.org/packages/9d/20/c6377a0d17434674351627489deca493ea0b137c522b99c81d3a106372c8/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6464150e25b68ae3cb04c4e57496ea11ebfaae4d98126aea2f4702dd43e3c12", size = 4219746, upload-time = "2025-12-06T17:33:33.097Z" },
- { url = "https://files.pythonhosted.org/packages/25/32/716c57b28eefe02a57a4c9d5bf956849597f5ea476c7010397199e56cfde/psycopg_binary-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:716a586f99bbe4f710dc58b40069fcb33c7627e95cc6fc936f73c9235e07f9cf", size = 3537494, upload-time = "2025-12-06T17:33:35.82Z" },
- { url = "https://files.pythonhosted.org/packages/14/73/7ca7cb22b9ac7393fb5de7d28ca97e8347c375c8498b3bff2c99c1f38038/psycopg_binary-3.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc5a189e89cbfff174588665bb18d28d2d0428366cc9dae5864afcaa2e57380b", size = 4579068, upload-time = "2025-12-06T17:33:39.303Z" },
- { url = "https://files.pythonhosted.org/packages/f5/42/0cf38ff6c62c792fc5b55398a853a77663210ebd51ed6f0c4a05b06f95a6/psycopg_binary-3.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:083c2e182be433f290dc2c516fd72b9b47054fcd305cce791e0a50d9e93e06f2", size = 4657520, upload-time = "2025-12-06T17:33:42.536Z" },
- { url = "https://files.pythonhosted.org/packages/3b/60/df846bc84cbf2231e01b0fff48b09841fe486fa177665e50f4995b1bfa44/psycopg_binary-3.3.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:ac230e3643d1c436a2dfb59ca84357dfc6862c9f372fc5dbd96bafecae581f9f", size = 5452086, upload-time = "2025-12-06T17:33:46.54Z" },
- { url = "https://files.pythonhosted.org/packages/ab/85/30c846a00db86b1b53fd5bfd4b4edfbd0c00de8f2c75dd105610bd7568fc/psycopg_binary-3.3.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d8c899a540f6c7585cee53cddc929dd4d2db90fd828e37f5d4017b63acbc1a5d", size = 5131125, upload-time = "2025-12-06T17:33:50.413Z" },
- { url = "https://files.pythonhosted.org/packages/6d/15/9968732013373f36f8a2a3fb76104dffc8efd9db78709caa5ae1a87b1f80/psycopg_binary-3.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50ff10ab8c0abdb5a5451b9315538865b50ba64c907742a1385fdf5f5772b73e", size = 6722914, upload-time = "2025-12-06T17:33:54.544Z" },
- { url = "https://files.pythonhosted.org/packages/b2/ba/29e361fe02143ac5ff5a1ca3e45697344cfbebe2eaf8c4e7eec164bff9a0/psycopg_binary-3.3.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:23d2594af848c1fd3d874a9364bef50730124e72df7bb145a20cb45e728c50ed", size = 4966081, upload-time = "2025-12-06T17:33:58.477Z" },
- { url = "https://files.pythonhosted.org/packages/99/45/1be90c8f1a1a237046903e91202fb06708745c179f220b361d6333ed7641/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ea4fe6b4ead3bbbe27244ea224fcd1f53cb119afc38b71a2f3ce570149a03e30", size = 4493332, upload-time = "2025-12-06T17:34:02.011Z" },
- { url = "https://files.pythonhosted.org/packages/2e/b5/bbdc07d5f0a5e90c617abd624368182aa131485e18038b2c6c85fc054aed/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:742ce48cde825b8e52fb1a658253d6d1ff66d152081cbc76aa45e2986534858d", size = 4170781, upload-time = "2025-12-06T17:34:05.298Z" },
- { url = "https://files.pythonhosted.org/packages/d1/2a/0d45e4f4da2bd78c3237ffa03475ef3751f69a81919c54a6e610eb1a7c96/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e22bf6b54df994aff37ab52695d635f1ef73155e781eee1f5fa75bc08b58c8da", size = 3910544, upload-time = "2025-12-06T17:34:08.251Z" },
- { url = "https://files.pythonhosted.org/packages/3a/62/a8e0f092f4dbef9a94b032fb71e214cf0a375010692fbe7493a766339e47/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8db9034cde3bcdafc66980f0130813f5c5d19e74b3f2a19fb3cfbc25ad113121", size = 4220070, upload-time = "2025-12-06T17:34:11.392Z" },
- { url = "https://files.pythonhosted.org/packages/09/e6/5fc8d8aff8afa114bb4a94a0341b9309311e8bf3ab32d816032f8b984d4e/psycopg_binary-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:df65174c7cf6b05ea273ce955927d3270b3a6e27b0b12762b009ce6082b8d3fc", size = 3540922, upload-time = "2025-12-06T17:34:14.88Z" },
- { url = "https://files.pythonhosted.org/packages/bd/75/ad18c0b97b852aba286d06befb398cc6d383e9dfd0a518369af275a5a526/psycopg_binary-3.3.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9ca24062cd9b2270e4d77576042e9cc2b1d543f09da5aba1f1a3d016cea28390", size = 4596371, upload-time = "2025-12-06T17:34:18.007Z" },
- { url = "https://files.pythonhosted.org/packages/5a/79/91649d94c8d89f84af5da7c9d474bfba35b08eb8f492ca3422b08f0a6427/psycopg_binary-3.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c749770da0947bc972e512f35366dd4950c0e34afad89e60b9787a37e97cb443", size = 4675139, upload-time = "2025-12-06T17:34:21.374Z" },
- { url = "https://files.pythonhosted.org/packages/56/ac/b26e004880f054549ec9396594e1ffe435810b0673e428e619ed722e4244/psycopg_binary-3.3.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03b7cd73fb8c45d272a34ae7249713e32492891492681e3cf11dff9531cf37e9", size = 5456120, upload-time = "2025-12-06T17:34:25.102Z" },
- { url = "https://files.pythonhosted.org/packages/4b/8d/410681dccd6f2999fb115cc248521ec50dd2b0aba66ae8de7e81efdebbee/psycopg_binary-3.3.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:43b130e3b6edcb5ee856c7167ccb8561b473308c870ed83978ae478613764f1c", size = 5133484, upload-time = "2025-12-06T17:34:28.933Z" },
- { url = "https://files.pythonhosted.org/packages/66/30/ebbab99ea2cfa099d7b11b742ce13415d44f800555bfa4ad2911dc645b71/psycopg_binary-3.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1feba5a8c617922321aef945865334e468337b8fc5c73074f5e63143013b5a", size = 6731818, upload-time = "2025-12-06T17:34:33.094Z" },
- { url = "https://files.pythonhosted.org/packages/70/02/d260646253b7ad805d60e0de47f9b811d6544078452579466a098598b6f4/psycopg_binary-3.3.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cabb2a554d9a0a6bf84037d86ca91782f087dfff2a61298d0b00c19c0bc43f6d", size = 4983859, upload-time = "2025-12-06T17:34:36.457Z" },
- { url = "https://files.pythonhosted.org/packages/72/8d/e778d7bad1a7910aa36281f092bd85c5702f508fd9bb0ea2020ffbb6585c/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74bc306c4b4df35b09bc8cecf806b271e1c5d708f7900145e4e54a2e5dedfed0", size = 4516388, upload-time = "2025-12-06T17:34:40.129Z" },
- { url = "https://files.pythonhosted.org/packages/bd/f1/64e82098722e2ab3521797584caf515284be09c1e08a872551b6edbb0074/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d79b0093f0fbf7a962d6a46ae292dc056c65d16a8ee9361f3cfbafd4c197ab14", size = 4192382, upload-time = "2025-12-06T17:34:43.279Z" },
- { url = "https://files.pythonhosted.org/packages/fa/d0/c20f4e668e89494972e551c31be2a0016e3f50d552d7ae9ac07086407599/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1586e220be05547c77afc326741dd41cc7fba38a81f9931f616ae98865439678", size = 3928660, upload-time = "2025-12-06T17:34:46.757Z" },
- { url = "https://files.pythonhosted.org/packages/0f/e1/99746c171de22539fd5eb1c9ca21dc805b54cfae502d7451d237d1dbc349/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:458696a5fa5dad5b6fb5d5862c22454434ce4fe1cf66ca6c0de5f904cbc1ae3e", size = 4239169, upload-time = "2025-12-06T17:34:49.751Z" },
- { url = "https://files.pythonhosted.org/packages/72/f7/212343c1c9cfac35fd943c527af85e9091d633176e2a407a0797856ff7b9/psycopg_binary-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:04bb2de4ba69d6f8395b446ede795e8884c040ec71d01dd07ac2b2d18d4153d1", size = 3642122, upload-time = "2025-12-06T17:34:52.506Z" },
-]
-
-[[package]]
-name = "pyasn1"
-version = "0.6.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" },
-]
-
-[[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.12.5"
-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/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
-]
-
-[[package]]
-name = "pydantic-core"
-version = "2.41.5"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
- { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
- { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
- { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" },
- { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" },
- { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" },
- { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" },
- { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" },
- { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" },
- { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" },
- { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" },
- { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" },
- { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" },
- { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" },
- { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
- { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
- { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
- { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
- { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
- { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
- { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
- { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
- { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
- { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
- { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
- { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
- { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
- { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
- { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
- { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
- { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
- { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
- { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
- { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
- { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
- { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
- { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
- { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
- { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
- { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
- { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
- { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
- { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
- { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
- { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
- { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
- { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
- { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
- { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
- { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
- { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
- { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
- { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
- { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
- { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
- { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
- { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
- { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
- { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
- { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
- { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
- { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
- { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
- { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
- { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
- { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
- { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
- { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
- { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
- { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
- { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
- { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
- { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
- { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" },
- { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
- { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
- { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
- { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
- { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
- { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
- { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
- { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" },
- { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" },
- { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" },
- { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" },
- { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
-]
-
-[[package]]
-name = "pydantic-settings"
-version = "2.12.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pydantic" },
- { name = "python-dotenv" },
- { name = "typing-inspection" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" },
-]
-
-[[package]]
-name = "pygit2"
-version = "1.19.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "cffi" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/17/49/cf8350817de19f4cafe4ae47881e38f56d9bbebaa9e5ef31a5458af4bcf8/pygit2-1.19.1.tar.gz", hash = "sha256:3165f784aae56a309a27d8eeae7923d53da2e8f6094308c7f5b428deec925cf9", size = 800869, upload-time = "2025-12-29T11:47:48.618Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f8/4f/c8c29c4af2de6b8b7e086cad24e200ec7f165587caa77b7d2d495366204e/pygit2-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2b54f3a94648ac8e287f5e4333710d9fe05f9e09de3da232d50df753bb01b643", size = 5702353, upload-time = "2025-12-29T11:46:28.548Z" },
- { url = "https://files.pythonhosted.org/packages/b9/04/814b305804f067fd8d1cd7166dc3704900704a8fa71280703212abbacf9f/pygit2-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e46618a912fc984b8a9f4d8322704620f1315264359c7fa61c899128e23e226", size = 5691612, upload-time = "2025-12-29T11:46:30.754Z" },
- { url = "https://files.pythonhosted.org/packages/cb/04/61c84d1ab2585f50a2551199e4228f3a800635c482e451e93f2cd0c0ae3d/pygit2-1.19.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2eb386b3e98f7056d76bc7e805e8fce3cd0a773cbbb30b0f7e144c0ac37270f2", size = 6021372, upload-time = "2025-12-29T11:46:32.439Z" },
- { url = "https://files.pythonhosted.org/packages/be/7a/daca8780c72b0d5a56165e0bff3b76d2fa8e0a8f7269f40aa17f10ed0356/pygit2-1.19.1-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f41a9b866676922ac9b0ec60f0dc9735a5d1ba6bb34146a6212dc0012d7959f", size = 4623817, upload-time = "2025-12-29T11:46:33.964Z" },
- { url = "https://files.pythonhosted.org/packages/92/f6/d065bb189c9fd86c5e540eb264567b4fe3eb06447da1408c03a35e15096b/pygit2-1.19.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2cdc81ecffd990d8c6dce44a16b1dc4494b5dd5381d6e1f508e459c4bca09e0", size = 5781284, upload-time = "2025-12-29T11:46:35.703Z" },
- { url = "https://files.pythonhosted.org/packages/ad/8a/2b9195619a9a0dc6e25525e784f7474174614ebc064a91b2a2087952a583/pygit2-1.19.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a1c8645287556aa9b670886dbc0d5daa1d49040511940822fd43dbda13cfe4e8", size = 6027281, upload-time = "2025-12-29T11:46:37.331Z" },
- { url = "https://files.pythonhosted.org/packages/d7/b7/20837029e8f5177d4ac48396a4448d02dfe455e988bb722d43dc42f6b0af/pygit2-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e388d1eb0c44d92d8ff01b25eb9a969fc28748966843c2e26e9e084e42567f7d", size = 5750642, upload-time = "2025-12-29T11:46:38.626Z" },
- { url = "https://files.pythonhosted.org/packages/41/42/18cc94976a35451a5653abf047356f94b5f503b1c0b02223a6d9e72979d3/pygit2-1.19.1-cp311-cp311-win32.whl", hash = "sha256:815c0b12845253929f2275759d623b3b4093e67e6536d2463177e6ff1d9ff0df", size = 942173, upload-time = "2025-12-29T11:46:40.087Z" },
- { url = "https://files.pythonhosted.org/packages/61/19/590708fc3182d47b40f0274f80671ccdf9c1a8fa5a838b554e6fe15a2bb3/pygit2-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:93f4986b35984aaaa5e7613ceb1ba4c184d890589df60b0d8d74e7dccec1d8cb", size = 1159463, upload-time = "2025-12-29T11:46:41.338Z" },
- { url = "https://files.pythonhosted.org/packages/90/a8/a2c1eb6f8c5f30cb5633a3c21e60ee6be2e4a3148b302f578e4b48e769ef/pygit2-1.19.1-cp311-cp311-win_arm64.whl", hash = "sha256:fef27b206955e66e3a63664e2ec93821e00ce2d917f8b4eae87c738163c00e14", size = 966795, upload-time = "2025-12-29T11:46:42.842Z" },
- { url = "https://files.pythonhosted.org/packages/1a/36/0784870218794d6069bf8ebae55679964edc44b8e59279f4526aa1220569/pygit2-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8e6a4f4a711750c286a13cea0007b40f7466c4d741c3d9b223ffbc3bbfbafe7", size = 5700218, upload-time = "2025-12-29T11:46:44.537Z" },
- { url = "https://files.pythonhosted.org/packages/56/65/47206823900ddca606022025369ba3e136de9d2310585acac10d8cef81fd/pygit2-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3f2340a668eb3e2d8927dcbeb1a043d3a65d2dd39a913995b34fc437da5e73af", size = 5692231, upload-time = "2025-12-29T11:46:45.821Z" },
- { url = "https://files.pythonhosted.org/packages/19/27/c6b52f53ee16b9d7eaacc575f08add3c336f53b5561cf94fe41ceeab1589/pygit2-1.19.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe41f09b1e334c43def6636b1133d2f4c91a20d9a6691bb4e7558e42a31bcb4e", size = 6022217, upload-time = "2025-12-29T11:46:47.086Z" },
- { url = "https://files.pythonhosted.org/packages/dc/ac/41d7a1ed69e117e9cd99b2f40f63898f9725ac6c4245b2b531ae0b7e59da/pygit2-1.19.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:527e57133d30ff6ea96634da6bf428f7d551958207fa73f9e9a18582b885e192", size = 4622846, upload-time = "2025-12-29T11:46:48.679Z" },
- { url = "https://files.pythonhosted.org/packages/09/22/f8fc7860b7b7ba15f7bf802ae3bce52b3e765b48846db115cb1c8372f971/pygit2-1.19.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a9340cb85b7be40080186a9d4dbf712a6be8a842556acbbfb305baebfb854f3", size = 5785236, upload-time = "2025-12-29T11:46:50.24Z" },
- { url = "https://files.pythonhosted.org/packages/ec/62/ee9275c48ecc119a7f5c48209aaa06d5f71d8476703c7700182c49c8a7a8/pygit2-1.19.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:66ecfa69f2287f50ec95dfc04821219c2f664c4cd292c7b33c10ed9afe975132", size = 6028266, upload-time = "2025-12-29T11:46:51.5Z" },
- { url = "https://files.pythonhosted.org/packages/7c/98/311112a50e6e319921f06c20ff237360c10bb2e8a1f959361567e48835f3/pygit2-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:14c76ec968ae20a6689c7b6fa833ef546c7bc176127d71e7b67cb2345a9813fb", size = 5755041, upload-time = "2025-12-29T11:46:53.337Z" },
- { url = "https://files.pythonhosted.org/packages/e1/45/f6a24326fb94e56ddae9906e21d4e4a006a36131a3a73819be1177e30e93/pygit2-1.19.1-cp312-cp312-win32.whl", hash = "sha256:ffe94118d39f6969fda594224b2b6df1ae79306adaf090ede65bcaf1a41b3a81", size = 942948, upload-time = "2025-12-29T11:46:54.465Z" },
- { url = "https://files.pythonhosted.org/packages/a3/1a/912ee3a33ba665f82cf8ed0087e7446f1f8e117aba1627e0c4ccc9b2a8c5/pygit2-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:c2ee3f2e91b0a5674ab7cb373234c23cf5f1cf6d84e56e6d12ff3db21414cf47", size = 1159880, upload-time = "2025-12-29T11:46:55.523Z" },
- { url = "https://files.pythonhosted.org/packages/24/fc/784eeceab43c2b4978aa46f03c267409f2502331fa18d0a8e58116d143d0/pygit2-1.19.1-cp312-cp312-win_arm64.whl", hash = "sha256:c8747d968d8d6b9d390263907f014d38a0f67bd26d8243e5bc3384cb252ec3d3", size = 966904, upload-time = "2025-12-29T11:46:56.888Z" },
- { url = "https://files.pythonhosted.org/packages/a0/2b/b3c8661e710ec49f7f38f992b913d6fef21e21ef6b9b327111b85bf1460c/pygit2-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:39af62f3e18dfdfb15c347c12b51231fdb3db3c9d5105d9046847ead14b42fce", size = 5700202, upload-time = "2025-12-29T11:46:58.294Z" },
- { url = "https://files.pythonhosted.org/packages/e2/1f/f67ec7f78a34ed14dbd3acf05ed23c4c8c2336ba6f3ca78d6b9962878435/pygit2-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed39106f1d9560709191093ed5251471dfb6b9e4aa35299dde45f4b91f7c984e", size = 5692171, upload-time = "2025-12-29T11:46:59.535Z" },
- { url = "https://files.pythonhosted.org/packages/a7/02/02f0f56b9b0b044018d9047adf68ba842ebda662ba43ace942ed904f8e9d/pygit2-1.19.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb4da746c92e23281890e865887d83f24e662fc3e1c481420e4993c5a13203fe", size = 6023018, upload-time = "2025-12-29T11:47:00.984Z" },
- { url = "https://files.pythonhosted.org/packages/da/a6/5ec78c14ca00fbffe6aa32eb6f5fbeb7fb06eb39e6929b06f7635f501a45/pygit2-1.19.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:93ccfab2340d38374f91ecf6cae6658bebc73883c376eb81eeb293781f6aef94", size = 4623392, upload-time = "2025-12-29T11:47:02.598Z" },
- { url = "https://files.pythonhosted.org/packages/d5/80/1a87f6e043e04cfa125380a73ef9f87a8c58292b7d4a6ed2e6203b4cd534/pygit2-1.19.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef18f1208422d3cac1c109417a5fc6143704cfff8e5de4e1665fa4a89ffe3902", size = 5786360, upload-time = "2025-12-29T11:47:03.898Z" },
- { url = "https://files.pythonhosted.org/packages/f7/6e/f5e38a4645d7fbba40083f94278814b9863b0afd14e905ebbd7ef31a27ec/pygit2-1.19.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:344f4c1e84eaa2434fbb43d96a1dd79796ab9559587a8533331fef92eab0ec7d", size = 6029576, upload-time = "2025-12-29T11:47:05.109Z" },
- { url = "https://files.pythonhosted.org/packages/53/cc/e5ff546f003c3fa635495105e3e039de3a863da66c82289b7a8baf6d5b48/pygit2-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1ae2f408206c67d395e8dc77425f8ab457cad59faaa58c700164398a62823e82", size = 5756457, upload-time = "2025-12-29T11:47:06.483Z" },
- { url = "https://files.pythonhosted.org/packages/da/dd/1331e3bdabd811992f511ebfa96f56c7b13d5f16837d74ac34dac93ce999/pygit2-1.19.1-cp313-cp313-win32.whl", hash = "sha256:9d6cf97c2da5c589b65371a8115be920cf417c46a80a2b12edb26e54a5238190", size = 942919, upload-time = "2025-12-29T11:47:07.833Z" },
- { url = "https://files.pythonhosted.org/packages/6d/01/98f74ecbe92f042d27e4de3cd7f093422d523cc67fdc74e6a65dbe4efbb8/pygit2-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:6d73aedffad280f6b655394e303533fcff15545d4d8f322011179c9474bb1b13", size = 1159846, upload-time = "2025-12-29T11:47:09.228Z" },
- { url = "https://files.pythonhosted.org/packages/27/4e/df8fa9a9f4e4e9aec417f8a674466d613985efb67453aa206f0455003738/pygit2-1.19.1-cp313-cp313-win_arm64.whl", hash = "sha256:8b067241c03a29440507e78637e233998fe1a11d2082169bd8177694ec4ee747", size = 966896, upload-time = "2025-12-29T11:47:10.233Z" },
- { url = "https://files.pythonhosted.org/packages/dd/45/1284c7714070b51e3413e66b677fa4ecf8c840d2f86d1bebc77d2390fe3d/pygit2-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d10a46285b9ae39b9de2d9f44ac7f933993aecfab189c2932320b3df596311c8", size = 5702338, upload-time = "2025-12-29T11:47:12.807Z" },
- { url = "https://files.pythonhosted.org/packages/a3/9f/7a39d4c612e12966130504e1610f500b397d7968feb6d25e1353614dab74/pygit2-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d0f3924d8d0d54a7fe186761c76dc1b6e5fcf41794a6daba1630db3bc216b9ba", size = 5692261, upload-time = "2025-12-29T11:47:14.276Z" },
- { url = "https://files.pythonhosted.org/packages/0b/7c/7806cf0ae9200bd773628be6d8c345d277b8f0161de950b572a4ce200105/pygit2-1.19.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4fcc301cfe9c29f3e29f0f80d81ae65c0bee368672b23566467dc91b5edae4b", size = 6025106, upload-time = "2025-12-29T11:47:15.904Z" },
- { url = "https://files.pythonhosted.org/packages/dc/30/7f1b67711705eb0220dcc4581a97b4aebad4ffde2f6f6b94314690e1cfa1/pygit2-1.19.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3c6eacf82f15e001121dc0f60057f462627045447d8bd8587b33b13159ae5155", size = 4627355, upload-time = "2025-12-29T11:47:17.365Z" },
- { url = "https://files.pythonhosted.org/packages/14/88/25f1e65ff6ed678e1be9aaeabeedcb26531d17b6b86c4b1d50d8f0c50825/pygit2-1.19.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:074b0b14c6f3c7e2c6ea0b01a90832407a71520c920918aa07f509c91f1691f9", size = 5788548, upload-time = "2025-12-29T11:47:18.98Z" },
- { url = "https://files.pythonhosted.org/packages/e7/5d/ff1b12d3682918ac6c3c6629a6c6272db1b4041994d38045d3c334034570/pygit2-1.19.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ada5d3e813e21918e004a33c66aba4a2b829cd5c0c0e85b92dd70f84cf95ac56", size = 6030078, upload-time = "2025-12-29T11:47:20.324Z" },
- { url = "https://files.pythonhosted.org/packages/1c/c5/c078ed6f1f5d7f3feba4b86d53e464c8358112ec32943e11e36557009818/pygit2-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:19ebe25fd8e95ed8a0be0a9dd4cecc1233db4f2a44a2a73984620909e98e907f", size = 5757154, upload-time = "2025-12-29T11:47:21.971Z" },
- { url = "https://files.pythonhosted.org/packages/76/90/1722d7c2db5d563becb59a54b2f49b44964ff699826629f96594064d972a/pygit2-1.19.1-cp314-cp314-win32.whl", hash = "sha256:5bc0738a49cceb76f0fba7cdb24532857a980e4a36b9a0da025c359dfe3676b4", size = 964159, upload-time = "2025-12-29T11:47:23.508Z" },
- { url = "https://files.pythonhosted.org/packages/74/72/80558b71ed780a732c9ff10003c3a73b68fbf320c3125ae11bb664a8076c/pygit2-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:527d40925bb85b86da0e96ecc90e9ca74d0a0273ab645bac0787b95923d93160", size = 1190612, upload-time = "2025-12-29T11:47:24.889Z" },
- { url = "https://files.pythonhosted.org/packages/28/68/c60ff9ae38543a520ca93c0d52a52c2e375ac44b9a5c5da99044cca8c5c5/pygit2-1.19.1-cp314-cp314-win_arm64.whl", hash = "sha256:21c7c8b5aa2f48cefdb8521185f0cd3c110a340e2d9f62a46a94db01a907db73", size = 994766, upload-time = "2025-12-29T11:47:25.902Z" },
- { url = "https://files.pythonhosted.org/packages/ee/42/4da546bf55183877e7da4327594ab138db92aa00921d46d513626bcad19f/pygit2-1.19.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9c5e4eb975b664b6821fe6a05b03bbc51052d1fb22f20652e1d4349ae24ed7ac", size = 5705642, upload-time = "2025-12-29T11:47:27.034Z" },
- { url = "https://files.pythonhosted.org/packages/0a/b7/74a9cf3d2e6cd6bd2fa6a7bc3530054c2f720fc59e3b731251bbdebd8983/pygit2-1.19.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8752eae5780ee51edae326cac394868917704624b63d03a5217c5e94a532a0e3", size = 5695192, upload-time = "2025-12-29T11:47:28.98Z" },
- { url = "https://files.pythonhosted.org/packages/b6/b9/bde02249c2c5deecc8e483ee9132f86f67114eec154ee10219d23a1ce9f9/pygit2-1.19.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:457f5a2e6d8527b5ad7a8bd16586c72ad2ce0aa218a37380f16d07520569ceaf", size = 6085318, upload-time = "2025-12-29T11:47:30.268Z" },
- { url = "https://files.pythonhosted.org/packages/d2/ae/b3a14edaa579700aee33a25a788f5f4fe67713a6e2273a897635e6742b35/pygit2-1.19.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3c8a9d53c84724c97d7e298f6628655c19f9911a90b88c362cb7d5daa645464f", size = 4684691, upload-time = "2025-12-29T11:47:31.829Z" },
- { url = "https://files.pythonhosted.org/packages/00/8d/5f557be149931ef7d692b66296a44263a1769070466eb1e63d6d1b3b97c1/pygit2-1.19.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d8442ad863be83be86baff006a6e11de3cddf17c7ee77eac2d389765987b554", size = 5841500, upload-time = "2025-12-29T11:47:33.636Z" },
- { url = "https://files.pythonhosted.org/packages/ce/f7/0101b3058e64df334c48193dfd6f1493a24b0c7813382c6b2e4db7a09ffa/pygit2-1.19.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ae9c775be518c7f20bf340091d329d3b9203cbd4273bf1b5505dc82dccf08147", size = 6087805, upload-time = "2025-12-29T11:47:34.926Z" },
- { url = "https://files.pythonhosted.org/packages/60/26/7d3fa88362b1703cd5b9bde411f37cded3b1f99dc83b720fc0c65ac8f37b/pygit2-1.19.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d5a45d466a4bc5d9eb0619ffc26b17e4018285e35ba9e2fe39576f13480b63bc", size = 5809156, upload-time = "2025-12-29T11:47:36.396Z" },
- { url = "https://files.pythonhosted.org/packages/90/38/f1952af3f61b3a7a49c417ffb67a5140c1183e6b04ec714c8941937860bf/pygit2-1.19.1-cp314-cp314t-win32.whl", hash = "sha256:6621acaaf2670e8fd0727c15271e5209a99769b127300ef7fc56b49babc8b1c1", size = 969317, upload-time = "2025-12-29T11:47:38.01Z" },
- { url = "https://files.pythonhosted.org/packages/31/02/205a4d10cb1195f6abf0a509883ede90caddefca6d9c3b54ef96e79e8e8a/pygit2-1.19.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4418dea6936fe3c1a9375d7cd31a69e72997e645e588ed31c40d785c71adde35", size = 1197068, upload-time = "2025-12-29T11:47:39.065Z" },
- { url = "https://files.pythonhosted.org/packages/00/20/4571edf9bebc9d60dcf5d5c3cd0a12e55a79b91b02ef960c44e4ffc24c70/pygit2-1.19.1-cp314-cp314t-win_arm64.whl", hash = "sha256:3cbb8ab952224c0b305aa56f8759bcad5d9a9de885b00fe0ff8bed9ac365472e", size = 995635, upload-time = "2025-12-29T11:47:40.327Z" },
- { url = "https://files.pythonhosted.org/packages/45/01/607b8a400ffe46340df083d67cb05296f90e0d302d09addac5dc1afee47f/pygit2-1.19.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3c56ef9ac89e020ca005a39db4e045792b1ce98c2450a53f79815e9d831c006a", size = 5646594, upload-time = "2025-12-29T11:47:41.437Z" },
- { url = "https://files.pythonhosted.org/packages/18/59/45e517b86692120fd927b8949916203c50ffce0cd7a7124131d90d816fde/pygit2-1.19.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a6d89079f3af32f25abb8680eabea31143a4f02f3d1da6644c296ba89b6a2fc", size = 5644506, upload-time = "2025-12-29T11:47:42.779Z" },
- { url = "https://files.pythonhosted.org/packages/db/25/41c0c37c0f8b23677364d9f82ddbb1377d2342666045d39b508acc3d6f97/pygit2-1.19.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bfd44dc6f1d5b1165cc2097c39000c4a5cc05443d27a3a5f2791ad338f52b07", size = 5559864, upload-time = "2025-12-29T11:47:44.399Z" },
- { url = "https://files.pythonhosted.org/packages/76/c0/16ff6c4d732d8644ab84a5d48141b55f6b353e08da5ffcbee03a5c58c3a5/pygit2-1.19.1-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0aca00ff7e3420f9c06d9386b0bfc76c18fd8a2c5234412db0e200a6cc47ed03", size = 5312681, upload-time = "2025-12-29T11:47:46.022Z" },
- { url = "https://files.pythonhosted.org/packages/08/cc/f762a2378d148ae40766fcac3f1ae1b5d925ae80128422366788eea9f5e6/pygit2-1.19.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f89f047667a218b71ebc96c398aca1e5109f149045a8d59ca9fd4a557d1e932e", size = 1130023, upload-time = "2025-12-29T11:47:47.55Z" },
-]
-
-[[package]]
-name = "pygments"
-version = "2.19.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
-]
-
-[[package]]
-name = "pyhumps"
-version = "3.8.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/c4/83/fa6f8fb7accb21f39e8f2b6a18f76f6d90626bdb0a5e5448e5cc9b8ab014/pyhumps-3.8.0.tar.gz", hash = "sha256:498026258f7ee1a8e447c2e28526c0bea9407f9a59c03260aee4bd6c04d681a3", size = 9018, upload-time = "2022-10-21T10:38:59.496Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9e/11/a1938340ecb32d71e47ad4914843775011e6e9da59ba1229f181fef3119e/pyhumps-3.8.0-py3-none-any.whl", hash = "sha256:060e1954d9069f428232a1adda165db0b9d8dfdce1d265d36df7fbff540acfd6", size = 6095, upload-time = "2022-10-21T10:38:58.231Z" },
-]
-
-[[package]]
-name = "pylint"
-version = "3.2.6"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "astroid" },
- { name = "colorama", marker = "sys_platform == 'win32'" },
- { name = "dill" },
- { name = "isort" },
- { name = "mccabe" },
- { name = "platformdirs" },
- { name = "tomlkit" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/30/10/abee071c1d52b2bca48be40fe9f64ca878a77e0beef6504597e8c9c1ed84/pylint-3.2.6.tar.gz", hash = "sha256:a5d01678349454806cff6d886fb072294f56a58c4761278c97fb557d708e1eb3", size = 1510167, upload-time = "2024-07-21T19:48:38.032Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/09/88/1a406dd0b17a4796f025d8c937d8d56f97869cffa55c21d9edb07f5a3912/pylint-3.2.6-py3-none-any.whl", hash = "sha256:03c8e3baa1d9fb995b12c1dbe00aa6c4bcef210c2a2634374aedeb22fb4a8f8f", size = 519798, upload-time = "2024-07-21T19:48:34.788Z" },
-]
-
-[[package]]
-name = "pytest"
-version = "9.0.2"
-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/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
-]
-
-[[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-mock"
-version = "3.15.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pytest" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" },
-]
-
-[[package]]
-name = "python-dateutil"
-version = "2.9.0.post0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "six" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
-]
-
-[[package]]
-name = "python-dotenv"
-version = "1.2.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
-]
-
-[[package]]
-name = "python-gitlab"
-version = "8.0.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "requests" },
- { name = "requests-toolbelt" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/c4/68/02645bc9d71554e7a263b118e4e55dafe4c4735c1ba74f9712232ed84054/python_gitlab-8.0.0.tar.gz", hash = "sha256:03eae5a9d105448796e6c0e192d402c266057e75790cf4f42c143dddf91313ce", size = 401334, upload-time = "2026-01-28T01:22:27.005Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/52/60/ba68e51e90a99b14af639463e5d617239029ec25927a0990ff28bd851916/python_gitlab-8.0.0-py3-none-any.whl", hash = "sha256:c635e6722c5710d35ddadfcf95c362b0aa8de11ab3972bc4f230ebd58a6c49ee", size = 144483, upload-time = "2026-01-28T01:22:25.772Z" },
-]
-
-[[package]]
-name = "pytokens"
-version = "0.4.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e5/16/4b9cfd90d55e66ffdb277d7ebe3bc25250c2311336ec3fc73b2673c794d5/pytokens-0.4.0.tar.gz", hash = "sha256:6b0b03e6ea7c9f9d47c5c61164b69ad30f4f0d70a5d9fe7eac4d19f24f77af2d", size = 15039, upload-time = "2026-01-19T07:59:50.623Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b4/05/3196399a353dd4cd99138a88f662810979ee2f1a1cdb0b417cb2f4507836/pytokens-0.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:92eb3ef88f27c22dc9dbab966ace4d61f6826e02ba04dac8e2d65ea31df56c8e", size = 160075, upload-time = "2026-01-19T07:59:00.316Z" },
- { url = "https://files.pythonhosted.org/packages/28/1d/c8fc4ed0a1c4f660391b201cda00b1d5bbcc00e2998e8bcd48b15eefd708/pytokens-0.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4b77858a680635ee9904306f54b0ee4781effb89e211ba0a773d76539537165", size = 247318, upload-time = "2026-01-19T07:59:01.636Z" },
- { url = "https://files.pythonhosted.org/packages/8e/0e/53e55ba01f3e858d229cd84b02481542f42ba59050483a78bf2447ee1af7/pytokens-0.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25cacc20c2ad90acb56f3739d87905473c54ca1fa5967ffcd675463fe965865e", size = 259752, upload-time = "2026-01-19T07:59:04.229Z" },
- { url = "https://files.pythonhosted.org/packages/dc/56/2d930d7f899e3f21868ca6e8ec739ac31e8fc532f66e09cbe45d3df0a84f/pytokens-0.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:628fab535ebc9079e4db35cd63cb401901c7ce8720a9834f9ad44b9eb4e0f1d4", size = 262842, upload-time = "2026-01-19T07:59:06.14Z" },
- { url = "https://files.pythonhosted.org/packages/42/dd/4e7e6920d23deffaf66e6f40d45f7610dcbc132ca5d90ab4faccef22f624/pytokens-0.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:4d0f568d7e82b7e96be56d03b5081de40e43c904eb6492bf09aaca47cd55f35b", size = 102620, upload-time = "2026-01-19T07:59:07.839Z" },
- { url = "https://files.pythonhosted.org/packages/3d/65/65460ebbfefd0bc1b160457904370d44f269e6e4582e0a9b6cba7c267b04/pytokens-0.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cd8da894e5a29ba6b6da8be06a4f7589d7220c099b5e363cb0643234b9b38c2a", size = 159864, upload-time = "2026-01-19T07:59:08.908Z" },
- { url = "https://files.pythonhosted.org/packages/25/70/a46669ec55876c392036b4da9808b5c3b1c5870bbca3d4cc923bf68bdbc1/pytokens-0.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:237ba7cfb677dbd3b01b09860810aceb448871150566b93cd24501d5734a04b1", size = 254448, upload-time = "2026-01-19T07:59:10.594Z" },
- { url = "https://files.pythonhosted.org/packages/62/0b/c486fc61299c2fc3b7f88ee4e115d4c8b6ffd1a7f88dc94b398b5b1bc4b8/pytokens-0.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01d1a61e36812e4e971cfe2c0e4c1f2d66d8311031dac8bf168af8a249fa04dd", size = 268863, upload-time = "2026-01-19T07:59:12.31Z" },
- { url = "https://files.pythonhosted.org/packages/79/92/b036af846707d25feaff7cafbd5280f1bd6a1034c16bb06a7c910209c1ab/pytokens-0.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e47e2ef3ec6ee86909e520d79f965f9b23389fda47460303cf715d510a6fe544", size = 267181, upload-time = "2026-01-19T07:59:13.856Z" },
- { url = "https://files.pythonhosted.org/packages/0d/c0/6d011fc00fefa74ce34816c84a923d2dd7c46b8dbc6ee52d13419786834c/pytokens-0.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d36954aba4557fd5a418a03cf595ecbb1cdcce119f91a49b19ef09d691a22ae", size = 102814, upload-time = "2026-01-19T07:59:15.288Z" },
- { url = "https://files.pythonhosted.org/packages/98/63/627b7e71d557383da5a97f473ad50f8d9c2c1f55c7d3c2531a120c796f6e/pytokens-0.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73eff3bdd8ad08da679867992782568db0529b887bed4c85694f84cdf35eafc6", size = 159744, upload-time = "2026-01-19T07:59:16.88Z" },
- { url = "https://files.pythonhosted.org/packages/28/d7/16f434c37ec3824eba6bcb6e798e5381a8dc83af7a1eda0f95c16fe3ade5/pytokens-0.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d97cc1f91b1a8e8ebccf31c367f28225699bea26592df27141deade771ed0afb", size = 253207, upload-time = "2026-01-19T07:59:18.069Z" },
- { url = "https://files.pythonhosted.org/packages/ab/96/04102856b9527701ae57d74a6393d1aca5bad18a1b1ca48ccffb3c93b392/pytokens-0.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c8952c537cb73a1a74369501a83b7f9d208c3cf92c41dd88a17814e68d48ce", size = 267452, upload-time = "2026-01-19T07:59:19.328Z" },
- { url = "https://files.pythonhosted.org/packages/0e/ef/0936eb472b89ab2d2c2c24bb81c50417e803fa89c731930d9fb01176fe9f/pytokens-0.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5dbf56f3c748aed9310b310d5b8b14e2c96d3ad682ad5a943f381bdbbdddf753", size = 265965, upload-time = "2026-01-19T07:59:20.613Z" },
- { url = "https://files.pythonhosted.org/packages/ae/f5/64f3d6f7df4a9e92ebda35ee85061f6260e16eac82df9396020eebbca775/pytokens-0.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:e131804513597f2dff2b18f9911d9b6276e21ef3699abeffc1c087c65a3d975e", size = 102813, upload-time = "2026-01-19T07:59:22.012Z" },
- { url = "https://files.pythonhosted.org/packages/5f/f1/d07e6209f18ef378fc2ae9dee8d1dfe91fd2447c2e2dbfa32867b6dd30cf/pytokens-0.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0d7374c917197106d3c4761374718bc55ea2e9ac0fb94171588ef5840ee1f016", size = 159968, upload-time = "2026-01-19T07:59:23.07Z" },
- { url = "https://files.pythonhosted.org/packages/0a/73/0eb111400abd382a04f253b269819db9fcc748aa40748441cebdcb6d068f/pytokens-0.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cd3fa1caf9e47a72ee134a29ca6b5bea84712724bba165d6628baa190c6ea5b", size = 253373, upload-time = "2026-01-19T07:59:24.381Z" },
- { url = "https://files.pythonhosted.org/packages/bd/8d/9e4e2fdb5bcaba679e54afcc304e9f13f488eb4d626e6b613f9553e03dbd/pytokens-0.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c6986576b7b07fe9791854caa5347923005a80b079d45b63b0be70d50cce5f1", size = 267024, upload-time = "2026-01-19T07:59:25.74Z" },
- { url = "https://files.pythonhosted.org/packages/cb/b7/e0a370321af2deb772cff14ff337e1140d1eac2c29a8876bfee995f486f0/pytokens-0.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9940f7c2e2f54fb1cb5fe17d0803c54da7a2bf62222704eb4217433664a186a7", size = 270912, upload-time = "2026-01-19T07:59:27.072Z" },
- { url = "https://files.pythonhosted.org/packages/7c/54/4348f916c440d4c3e68b53b4ed0e66b292d119e799fa07afa159566dcc86/pytokens-0.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:54691cf8f299e7efabcc25adb4ce715d3cef1491e1c930eaf555182f898ef66a", size = 103836, upload-time = "2026-01-19T07:59:28.112Z" },
- { url = "https://files.pythonhosted.org/packages/e8/f8/a693c0cfa9c783a2a8c4500b7b2a8bab420f8ca4f2d496153226bf1c12e3/pytokens-0.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:94ff5db97a0d3cd7248a5b07ba2167bd3edc1db92f76c6db00137bbaf068ddf8", size = 167643, upload-time = "2026-01-19T07:59:29.292Z" },
- { url = "https://files.pythonhosted.org/packages/c0/dd/a64eb1e9f3ec277b69b33ef1b40ffbcc8f0a3bafcde120997efc7bdefebf/pytokens-0.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0dd6261cd9cc95fae1227b1b6ebee023a5fd4a4b6330b071c73a516f5f59b63", size = 289553, upload-time = "2026-01-19T07:59:30.537Z" },
- { url = "https://files.pythonhosted.org/packages/df/22/06c1079d93dbc3bca5d013e1795f3d8b9ed6c87290acd6913c1c526a6bb2/pytokens-0.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cdca8159df407dbd669145af4171a0d967006e0be25f3b520896bc7068f02c4", size = 302490, upload-time = "2026-01-19T07:59:32.352Z" },
- { url = "https://files.pythonhosted.org/packages/8d/de/a6f5e43115b4fbf4b93aa87d6c83c79932cdb084f9711daae04549e1e4ad/pytokens-0.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4b5770abeb2a24347380a1164a558f0ebe06e98aedbd54c45f7929527a5fb26e", size = 305652, upload-time = "2026-01-19T07:59:33.685Z" },
- { url = "https://files.pythonhosted.org/packages/ab/3d/c136e057cb622e36e0c3ff7a8aaa19ff9720050c4078235691da885fe6ee/pytokens-0.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:74500d72c561dad14c037a9e86a657afd63e277dd5a3bb7570932ab7a3b12551", size = 115472, upload-time = "2026-01-19T07:59:34.734Z" },
- { url = "https://files.pythonhosted.org/packages/7c/3c/6941a82f4f130af6e1c68c076b6789069ef10c04559bd4733650f902fd3b/pytokens-0.4.0-py3-none-any.whl", hash = "sha256:0508d11b4de157ee12063901603be87fb0253e8f4cb9305eb168b1202ab92068", size = 13224, upload-time = "2026-01-19T07:59:49.822Z" },
-]
-
-[[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-ft"
-version = "8.0.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/5e/eb/5a0d575de784f9a1f94e2b1288c6886f13f34185e13117ed530f32b6f8a8/pyyaml_ft-8.0.0.tar.gz", hash = "sha256:0c947dce03954c7b5d38869ed4878b2e6ff1d44b08a0d84dc83fdad205ae39ab", size = 141057, upload-time = "2025-06-10T15:32:15.613Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/68/ba/a067369fe61a2e57fb38732562927d5bae088c73cb9bb5438736a9555b29/pyyaml_ft-8.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c1306282bc958bfda31237f900eb52c9bedf9b93a11f82e1aab004c9a5657a6", size = 187027, upload-time = "2025-06-10T15:31:48.722Z" },
- { url = "https://files.pythonhosted.org/packages/ad/c5/a3d2020ce5ccfc6aede0d45bcb870298652ac0cf199f67714d250e0cdf39/pyyaml_ft-8.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30c5f1751625786c19de751e3130fc345ebcba6a86f6bddd6e1285342f4bbb69", size = 176146, upload-time = "2025-06-10T15:31:50.584Z" },
- { url = "https://files.pythonhosted.org/packages/e3/bb/23a9739291086ca0d3189eac7cd92b4d00e9fdc77d722ab610c35f9a82ba/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fa992481155ddda2e303fcc74c79c05eddcdbc907b888d3d9ce3ff3e2adcfb0", size = 746792, upload-time = "2025-06-10T15:31:52.304Z" },
- { url = "https://files.pythonhosted.org/packages/5f/c2/e8825f4ff725b7e560d62a3609e31d735318068e1079539ebfde397ea03e/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cec6c92b4207004b62dfad1f0be321c9f04725e0f271c16247d8b39c3bf3ea42", size = 786772, upload-time = "2025-06-10T15:31:54.712Z" },
- { url = "https://files.pythonhosted.org/packages/35/be/58a4dcae8854f2fdca9b28d9495298fd5571a50d8430b1c3033ec95d2d0e/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06237267dbcab70d4c0e9436d8f719f04a51123f0ca2694c00dd4b68c338e40b", size = 778723, upload-time = "2025-06-10T15:31:56.093Z" },
- { url = "https://files.pythonhosted.org/packages/86/ed/fed0da92b5d5d7340a082e3802d84c6dc9d5fa142954404c41a544c1cb92/pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a7f332bc565817644cdb38ffe4739e44c3e18c55793f75dddb87630f03fc254", size = 758478, upload-time = "2025-06-10T15:31:58.314Z" },
- { url = "https://files.pythonhosted.org/packages/f0/69/ac02afe286275980ecb2dcdc0156617389b7e0c0a3fcdedf155c67be2b80/pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7d10175a746be65f6feb86224df5d6bc5c049ebf52b89a88cf1cd78af5a367a8", size = 799159, upload-time = "2025-06-10T15:31:59.675Z" },
- { url = "https://files.pythonhosted.org/packages/4e/ac/c492a9da2e39abdff4c3094ec54acac9747743f36428281fb186a03fab76/pyyaml_ft-8.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:58e1015098cf8d8aec82f360789c16283b88ca670fe4275ef6c48c5e30b22a96", size = 158779, upload-time = "2025-06-10T15:32:01.029Z" },
- { url = "https://files.pythonhosted.org/packages/5d/9b/41998df3298960d7c67653669f37710fa2d568a5fc933ea24a6df60acaf6/pyyaml_ft-8.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5f3e2ceb790d50602b2fd4ec37abbd760a8c778e46354df647e7c5a4ebb", size = 191331, upload-time = "2025-06-10T15:32:02.602Z" },
- { url = "https://files.pythonhosted.org/packages/0f/16/2710c252ee04cbd74d9562ebba709e5a284faeb8ada88fcda548c9191b47/pyyaml_ft-8.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d445bf6ea16bb93c37b42fdacfb2f94c8e92a79ba9e12768c96ecde867046d1", size = 182879, upload-time = "2025-06-10T15:32:04.466Z" },
- { url = "https://files.pythonhosted.org/packages/9a/40/ae8163519d937fa7bfa457b6f78439cc6831a7c2b170e4f612f7eda71815/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c56bb46b4fda34cbb92a9446a841da3982cdde6ea13de3fbd80db7eeeab8b49", size = 811277, upload-time = "2025-06-10T15:32:06.214Z" },
- { url = "https://files.pythonhosted.org/packages/f9/66/28d82dbff7f87b96f0eeac79b7d972a96b4980c1e445eb6a857ba91eda00/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dab0abb46eb1780da486f022dce034b952c8ae40753627b27a626d803926483b", size = 831650, upload-time = "2025-06-10T15:32:08.076Z" },
- { url = "https://files.pythonhosted.org/packages/e8/df/161c4566facac7d75a9e182295c223060373d4116dead9cc53a265de60b9/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd48d639cab5ca50ad957b6dd632c7dd3ac02a1abe0e8196a3c24a52f5db3f7a", size = 815755, upload-time = "2025-06-10T15:32:09.435Z" },
- { url = "https://files.pythonhosted.org/packages/05/10/f42c48fa5153204f42eaa945e8d1fd7c10d6296841dcb2447bf7da1be5c4/pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:052561b89d5b2a8e1289f326d060e794c21fa068aa11255fe71d65baf18a632e", size = 810403, upload-time = "2025-06-10T15:32:11.051Z" },
- { url = "https://files.pythonhosted.org/packages/d5/d2/e369064aa51009eb9245399fd8ad2c562bd0bcd392a00be44b2a824ded7c/pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3bb4b927929b0cb162fb1605392a321e3333e48ce616cdcfa04a839271373255", size = 835581, upload-time = "2025-06-10T15:32:12.897Z" },
- { url = "https://files.pythonhosted.org/packages/c0/28/26534bed77109632a956977f60d8519049f545abc39215d086e33a61f1f2/pyyaml_ft-8.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:de04cfe9439565e32f178106c51dd6ca61afaa2907d143835d501d84703d3793", size = 171579, upload-time = "2025-06-10T15:32:14.34Z" },
-]
-
-[[package]]
-name = "regex"
-version = "2026.1.15"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d0/c9/0c80c96eab96948363d270143138d671d5731c3a692b417629bf3492a9d6/regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a", size = 488168, upload-time = "2026-01-14T23:14:16.129Z" },
- { url = "https://files.pythonhosted.org/packages/17/f0/271c92f5389a552494c429e5cc38d76d1322eb142fb5db3c8ccc47751468/regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f", size = 290636, upload-time = "2026-01-14T23:14:17.715Z" },
- { url = "https://files.pythonhosted.org/packages/a0/f9/5f1fd077d106ca5655a0f9ff8f25a1ab55b92128b5713a91ed7134ff688e/regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1", size = 288496, upload-time = "2026-01-14T23:14:19.326Z" },
- { url = "https://files.pythonhosted.org/packages/b5/e1/8f43b03a4968c748858ec77f746c286d81f896c2e437ccf050ebc5d3128c/regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b", size = 793503, upload-time = "2026-01-14T23:14:20.922Z" },
- { url = "https://files.pythonhosted.org/packages/8d/4e/a39a5e8edc5377a46a7c875c2f9a626ed3338cb3bb06931be461c3e1a34a/regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8", size = 860535, upload-time = "2026-01-14T23:14:22.405Z" },
- { url = "https://files.pythonhosted.org/packages/dc/1c/9dce667a32a9477f7a2869c1c767dc00727284a9fa3ff5c09a5c6c03575e/regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413", size = 907225, upload-time = "2026-01-14T23:14:23.897Z" },
- { url = "https://files.pythonhosted.org/packages/a4/3c/87ca0a02736d16b6262921425e84b48984e77d8e4e572c9072ce96e66c30/regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026", size = 800526, upload-time = "2026-01-14T23:14:26.039Z" },
- { url = "https://files.pythonhosted.org/packages/4b/ff/647d5715aeea7c87bdcbd2f578f47b415f55c24e361e639fe8c0cc88878f/regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785", size = 773446, upload-time = "2026-01-14T23:14:28.109Z" },
- { url = "https://files.pythonhosted.org/packages/af/89/bf22cac25cb4ba0fe6bff52ebedbb65b77a179052a9d6037136ae93f42f4/regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e", size = 783051, upload-time = "2026-01-14T23:14:29.929Z" },
- { url = "https://files.pythonhosted.org/packages/1e/f4/6ed03e71dca6348a5188363a34f5e26ffd5db1404780288ff0d79513bce4/regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763", size = 854485, upload-time = "2026-01-14T23:14:31.366Z" },
- { url = "https://files.pythonhosted.org/packages/d9/9a/8e8560bd78caded8eb137e3e47612430a05b9a772caf60876435192d670a/regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb", size = 762195, upload-time = "2026-01-14T23:14:32.802Z" },
- { url = "https://files.pythonhosted.org/packages/38/6b/61fc710f9aa8dfcd764fe27d37edfaa023b1a23305a0d84fccd5adb346ea/regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2", size = 845986, upload-time = "2026-01-14T23:14:34.898Z" },
- { url = "https://files.pythonhosted.org/packages/fd/2e/fbee4cb93f9d686901a7ca8d94285b80405e8c34fe4107f63ffcbfb56379/regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1", size = 788992, upload-time = "2026-01-14T23:14:37.116Z" },
- { url = "https://files.pythonhosted.org/packages/ed/14/3076348f3f586de64b1ab75a3fbabdaab7684af7f308ad43be7ef1849e55/regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569", size = 265893, upload-time = "2026-01-14T23:14:38.426Z" },
- { url = "https://files.pythonhosted.org/packages/0f/19/772cf8b5fc803f5c89ba85d8b1870a1ca580dc482aa030383a9289c82e44/regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7", size = 277840, upload-time = "2026-01-14T23:14:39.785Z" },
- { url = "https://files.pythonhosted.org/packages/78/84/d05f61142709474da3c0853222d91086d3e1372bcdab516c6fd8d80f3297/regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec", size = 270374, upload-time = "2026-01-14T23:14:41.592Z" },
- { url = "https://files.pythonhosted.org/packages/92/81/10d8cf43c807d0326efe874c1b79f22bfb0fb226027b0b19ebc26d301408/regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1", size = 489398, upload-time = "2026-01-14T23:14:43.741Z" },
- { url = "https://files.pythonhosted.org/packages/90/b0/7c2a74e74ef2a7c32de724658a69a862880e3e4155cba992ba04d1c70400/regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681", size = 291339, upload-time = "2026-01-14T23:14:45.183Z" },
- { url = "https://files.pythonhosted.org/packages/19/4d/16d0773d0c818417f4cc20aa0da90064b966d22cd62a8c46765b5bd2d643/regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f", size = 289003, upload-time = "2026-01-14T23:14:47.25Z" },
- { url = "https://files.pythonhosted.org/packages/c6/e4/1fc4599450c9f0863d9406e944592d968b8d6dfd0d552a7d569e43bceada/regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa", size = 798656, upload-time = "2026-01-14T23:14:48.77Z" },
- { url = "https://files.pythonhosted.org/packages/b2/e6/59650d73a73fa8a60b3a590545bfcf1172b4384a7df2e7fe7b9aab4e2da9/regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804", size = 864252, upload-time = "2026-01-14T23:14:50.528Z" },
- { url = "https://files.pythonhosted.org/packages/6e/ab/1d0f4d50a1638849a97d731364c9a80fa304fec46325e48330c170ee8e80/regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c", size = 912268, upload-time = "2026-01-14T23:14:52.952Z" },
- { url = "https://files.pythonhosted.org/packages/dd/df/0d722c030c82faa1d331d1921ee268a4e8fb55ca8b9042c9341c352f17fa/regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5", size = 803589, upload-time = "2026-01-14T23:14:55.182Z" },
- { url = "https://files.pythonhosted.org/packages/66/23/33289beba7ccb8b805c6610a8913d0131f834928afc555b241caabd422a9/regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3", size = 775700, upload-time = "2026-01-14T23:14:56.707Z" },
- { url = "https://files.pythonhosted.org/packages/e7/65/bf3a42fa6897a0d3afa81acb25c42f4b71c274f698ceabd75523259f6688/regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb", size = 787928, upload-time = "2026-01-14T23:14:58.312Z" },
- { url = "https://files.pythonhosted.org/packages/f4/f5/13bf65864fc314f68cdd6d8ca94adcab064d4d39dbd0b10fef29a9da48fc/regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410", size = 858607, upload-time = "2026-01-14T23:15:00.657Z" },
- { url = "https://files.pythonhosted.org/packages/a3/31/040e589834d7a439ee43fb0e1e902bc81bd58a5ba81acffe586bb3321d35/regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4", size = 763729, upload-time = "2026-01-14T23:15:02.248Z" },
- { url = "https://files.pythonhosted.org/packages/9b/84/6921e8129687a427edf25a34a5594b588b6d88f491320b9de5b6339a4fcb/regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d", size = 850697, upload-time = "2026-01-14T23:15:03.878Z" },
- { url = "https://files.pythonhosted.org/packages/8a/87/3d06143d4b128f4229158f2de5de6c8f2485170c7221e61bf381313314b2/regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22", size = 789849, upload-time = "2026-01-14T23:15:06.102Z" },
- { url = "https://files.pythonhosted.org/packages/77/69/c50a63842b6bd48850ebc7ab22d46e7a2a32d824ad6c605b218441814639/regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913", size = 266279, upload-time = "2026-01-14T23:15:07.678Z" },
- { url = "https://files.pythonhosted.org/packages/f2/36/39d0b29d087e2b11fd8191e15e81cce1b635fcc845297c67f11d0d19274d/regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a", size = 277166, upload-time = "2026-01-14T23:15:09.257Z" },
- { url = "https://files.pythonhosted.org/packages/28/32/5b8e476a12262748851fa8ab1b0be540360692325975b094e594dfebbb52/regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056", size = 270415, upload-time = "2026-01-14T23:15:10.743Z" },
- { url = "https://files.pythonhosted.org/packages/f8/2e/6870bb16e982669b674cce3ee9ff2d1d46ab80528ee6bcc20fb2292efb60/regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e", size = 489164, upload-time = "2026-01-14T23:15:13.962Z" },
- { url = "https://files.pythonhosted.org/packages/dc/67/9774542e203849b0286badf67199970a44ebdb0cc5fb739f06e47ada72f8/regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10", size = 291218, upload-time = "2026-01-14T23:15:15.647Z" },
- { url = "https://files.pythonhosted.org/packages/b2/87/b0cda79f22b8dee05f774922a214da109f9a4c0eca5da2c9d72d77ea062c/regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc", size = 288895, upload-time = "2026-01-14T23:15:17.788Z" },
- { url = "https://files.pythonhosted.org/packages/3b/6a/0041f0a2170d32be01ab981d6346c83a8934277d82c780d60b127331f264/regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599", size = 798680, upload-time = "2026-01-14T23:15:19.342Z" },
- { url = "https://files.pythonhosted.org/packages/58/de/30e1cfcdbe3e891324aa7568b7c968771f82190df5524fabc1138cb2d45a/regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae", size = 864210, upload-time = "2026-01-14T23:15:22.005Z" },
- { url = "https://files.pythonhosted.org/packages/64/44/4db2f5c5ca0ccd40ff052ae7b1e9731352fcdad946c2b812285a7505ca75/regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5", size = 912358, upload-time = "2026-01-14T23:15:24.569Z" },
- { url = "https://files.pythonhosted.org/packages/79/b6/e6a5665d43a7c42467138c8a2549be432bad22cbd206f5ec87162de74bd7/regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6", size = 803583, upload-time = "2026-01-14T23:15:26.526Z" },
- { url = "https://files.pythonhosted.org/packages/e7/53/7cd478222169d85d74d7437e74750005e993f52f335f7c04ff7adfda3310/regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788", size = 775782, upload-time = "2026-01-14T23:15:29.352Z" },
- { url = "https://files.pythonhosted.org/packages/ca/b5/75f9a9ee4b03a7c009fe60500fe550b45df94f0955ca29af16333ef557c5/regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714", size = 787978, upload-time = "2026-01-14T23:15:31.295Z" },
- { url = "https://files.pythonhosted.org/packages/72/b3/79821c826245bbe9ccbb54f6eadb7879c722fd3e0248c17bfc90bf54e123/regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d", size = 858550, upload-time = "2026-01-14T23:15:33.558Z" },
- { url = "https://files.pythonhosted.org/packages/4a/85/2ab5f77a1c465745bfbfcb3ad63178a58337ae8d5274315e2cc623a822fa/regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3", size = 763747, upload-time = "2026-01-14T23:15:35.206Z" },
- { url = "https://files.pythonhosted.org/packages/6d/84/c27df502d4bfe2873a3e3a7cf1bdb2b9cc10284d1a44797cf38bed790470/regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31", size = 850615, upload-time = "2026-01-14T23:15:37.523Z" },
- { url = "https://files.pythonhosted.org/packages/7d/b7/658a9782fb253680aa8ecb5ccbb51f69e088ed48142c46d9f0c99b46c575/regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3", size = 789951, upload-time = "2026-01-14T23:15:39.582Z" },
- { url = "https://files.pythonhosted.org/packages/fc/2a/5928af114441e059f15b2f63e188bd00c6529b3051c974ade7444b85fcda/regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f", size = 266275, upload-time = "2026-01-14T23:15:42.108Z" },
- { url = "https://files.pythonhosted.org/packages/4f/16/5bfbb89e435897bff28cf0352a992ca719d9e55ebf8b629203c96b6ce4f7/regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e", size = 277145, upload-time = "2026-01-14T23:15:44.244Z" },
- { url = "https://files.pythonhosted.org/packages/56/c1/a09ff7392ef4233296e821aec5f78c51be5e91ffde0d163059e50fd75835/regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337", size = 270411, upload-time = "2026-01-14T23:15:45.858Z" },
- { url = "https://files.pythonhosted.org/packages/3c/38/0cfd5a78e5c6db00e6782fdae70458f89850ce95baa5e8694ab91d89744f/regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be", size = 492068, upload-time = "2026-01-14T23:15:47.616Z" },
- { url = "https://files.pythonhosted.org/packages/50/72/6c86acff16cb7c959c4355826bbf06aad670682d07c8f3998d9ef4fee7cd/regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8", size = 292756, upload-time = "2026-01-14T23:15:49.307Z" },
- { url = "https://files.pythonhosted.org/packages/4e/58/df7fb69eadfe76526ddfce28abdc0af09ffe65f20c2c90932e89d705153f/regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd", size = 291114, upload-time = "2026-01-14T23:15:51.484Z" },
- { url = "https://files.pythonhosted.org/packages/ed/6c/a4011cd1cf96b90d2cdc7e156f91efbd26531e822a7fbb82a43c1016678e/regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a", size = 807524, upload-time = "2026-01-14T23:15:53.102Z" },
- { url = "https://files.pythonhosted.org/packages/1d/25/a53ffb73183f69c3e9f4355c4922b76d2840aee160af6af5fac229b6201d/regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93", size = 873455, upload-time = "2026-01-14T23:15:54.956Z" },
- { url = "https://files.pythonhosted.org/packages/66/0b/8b47fc2e8f97d9b4a851736f3890a5f786443aa8901061c55f24c955f45b/regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af", size = 915007, upload-time = "2026-01-14T23:15:57.041Z" },
- { url = "https://files.pythonhosted.org/packages/c2/fa/97de0d681e6d26fabe71968dbee06dd52819e9a22fdce5dac7256c31ed84/regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09", size = 812794, upload-time = "2026-01-14T23:15:58.916Z" },
- { url = "https://files.pythonhosted.org/packages/22/38/e752f94e860d429654aa2b1c51880bff8dfe8f084268258adf9151cf1f53/regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5", size = 781159, upload-time = "2026-01-14T23:16:00.817Z" },
- { url = "https://files.pythonhosted.org/packages/e9/a7/d739ffaef33c378fc888302a018d7f81080393d96c476b058b8c64fd2b0d/regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794", size = 795558, upload-time = "2026-01-14T23:16:03.267Z" },
- { url = "https://files.pythonhosted.org/packages/3e/c4/542876f9a0ac576100fc73e9c75b779f5c31e3527576cfc9cb3009dcc58a/regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a", size = 868427, upload-time = "2026-01-14T23:16:05.646Z" },
- { url = "https://files.pythonhosted.org/packages/fc/0f/d5655bea5b22069e32ae85a947aa564912f23758e112cdb74212848a1a1b/regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80", size = 769939, upload-time = "2026-01-14T23:16:07.542Z" },
- { url = "https://files.pythonhosted.org/packages/20/06/7e18a4fa9d326daeda46d471a44ef94201c46eaa26dbbb780b5d92cbfdda/regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2", size = 854753, upload-time = "2026-01-14T23:16:10.395Z" },
- { url = "https://files.pythonhosted.org/packages/3b/67/dc8946ef3965e166f558ef3b47f492bc364e96a265eb4a2bb3ca765c8e46/regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60", size = 799559, upload-time = "2026-01-14T23:16:12.347Z" },
- { url = "https://files.pythonhosted.org/packages/a5/61/1bba81ff6d50c86c65d9fd84ce9699dd106438ee4cdb105bf60374ee8412/regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952", size = 268879, upload-time = "2026-01-14T23:16:14.049Z" },
- { url = "https://files.pythonhosted.org/packages/e9/5e/cef7d4c5fb0ea3ac5c775fd37db5747f7378b29526cc83f572198924ff47/regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10", size = 280317, upload-time = "2026-01-14T23:16:15.718Z" },
- { url = "https://files.pythonhosted.org/packages/b4/52/4317f7a5988544e34ab57b4bde0f04944c4786128c933fb09825924d3e82/regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829", size = 271551, upload-time = "2026-01-14T23:16:17.533Z" },
- { url = "https://files.pythonhosted.org/packages/52/0a/47fa888ec7cbbc7d62c5f2a6a888878e76169170ead271a35239edd8f0e8/regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac", size = 489170, upload-time = "2026-01-14T23:16:19.835Z" },
- { url = "https://files.pythonhosted.org/packages/ac/c4/d000e9b7296c15737c9301708e9e7fbdea009f8e93541b6b43bdb8219646/regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6", size = 291146, upload-time = "2026-01-14T23:16:21.541Z" },
- { url = "https://files.pythonhosted.org/packages/f9/b6/921cc61982e538682bdf3bdf5b2c6ab6b34368da1f8e98a6c1ddc503c9cf/regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2", size = 288986, upload-time = "2026-01-14T23:16:23.381Z" },
- { url = "https://files.pythonhosted.org/packages/ca/33/eb7383dde0bbc93f4fb9d03453aab97e18ad4024ac7e26cef8d1f0a2cff0/regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846", size = 799098, upload-time = "2026-01-14T23:16:25.088Z" },
- { url = "https://files.pythonhosted.org/packages/27/56/b664dccae898fc8d8b4c23accd853f723bde0f026c747b6f6262b688029c/regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b", size = 864980, upload-time = "2026-01-14T23:16:27.297Z" },
- { url = "https://files.pythonhosted.org/packages/16/40/0999e064a170eddd237bae9ccfcd8f28b3aa98a38bf727a086425542a4fc/regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e", size = 911607, upload-time = "2026-01-14T23:16:29.235Z" },
- { url = "https://files.pythonhosted.org/packages/07/78/c77f644b68ab054e5a674fb4da40ff7bffb2c88df58afa82dbf86573092d/regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde", size = 803358, upload-time = "2026-01-14T23:16:31.369Z" },
- { url = "https://files.pythonhosted.org/packages/27/31/d4292ea8566eaa551fafc07797961c5963cf5235c797cc2ae19b85dfd04d/regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5", size = 775833, upload-time = "2026-01-14T23:16:33.141Z" },
- { url = "https://files.pythonhosted.org/packages/ce/b2/cff3bf2fea4133aa6fb0d1e370b37544d18c8350a2fa118c7e11d1db0e14/regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34", size = 788045, upload-time = "2026-01-14T23:16:35.005Z" },
- { url = "https://files.pythonhosted.org/packages/8d/99/2cb9b69045372ec877b6f5124bda4eb4253bc58b8fe5848c973f752bc52c/regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75", size = 859374, upload-time = "2026-01-14T23:16:36.919Z" },
- { url = "https://files.pythonhosted.org/packages/09/16/710b0a5abe8e077b1729a562d2f297224ad079f3a66dce46844c193416c8/regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e", size = 763940, upload-time = "2026-01-14T23:16:38.685Z" },
- { url = "https://files.pythonhosted.org/packages/dd/d1/7585c8e744e40eb3d32f119191969b91de04c073fca98ec14299041f6e7e/regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160", size = 850112, upload-time = "2026-01-14T23:16:40.646Z" },
- { url = "https://files.pythonhosted.org/packages/af/d6/43e1dd85df86c49a347aa57c1f69d12c652c7b60e37ec162e3096194a278/regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1", size = 789586, upload-time = "2026-01-14T23:16:42.799Z" },
- { url = "https://files.pythonhosted.org/packages/93/38/77142422f631e013f316aaae83234c629555729a9fbc952b8a63ac91462a/regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1", size = 271691, upload-time = "2026-01-14T23:16:44.671Z" },
- { url = "https://files.pythonhosted.org/packages/4a/a9/ab16b4649524ca9e05213c1cdbb7faa85cc2aa90a0230d2f796cbaf22736/regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903", size = 280422, upload-time = "2026-01-14T23:16:46.607Z" },
- { url = "https://files.pythonhosted.org/packages/be/2a/20fd057bf3521cb4791f69f869635f73e0aaf2b9ad2d260f728144f9047c/regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705", size = 273467, upload-time = "2026-01-14T23:16:48.967Z" },
- { url = "https://files.pythonhosted.org/packages/ad/77/0b1e81857060b92b9cad239104c46507dd481b3ff1fa79f8e7f865aae38a/regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8", size = 492073, upload-time = "2026-01-14T23:16:51.154Z" },
- { url = "https://files.pythonhosted.org/packages/70/f3/f8302b0c208b22c1e4f423147e1913fd475ddd6230565b299925353de644/regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf", size = 292757, upload-time = "2026-01-14T23:16:53.08Z" },
- { url = "https://files.pythonhosted.org/packages/bf/f0/ef55de2460f3b4a6da9d9e7daacd0cb79d4ef75c64a2af316e68447f0df0/regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d", size = 291122, upload-time = "2026-01-14T23:16:55.383Z" },
- { url = "https://files.pythonhosted.org/packages/cf/55/bb8ccbacabbc3a11d863ee62a9f18b160a83084ea95cdfc5d207bfc3dd75/regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84", size = 807761, upload-time = "2026-01-14T23:16:57.251Z" },
- { url = "https://files.pythonhosted.org/packages/8f/84/f75d937f17f81e55679a0509e86176e29caa7298c38bd1db7ce9c0bf6075/regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df", size = 873538, upload-time = "2026-01-14T23:16:59.349Z" },
- { url = "https://files.pythonhosted.org/packages/b8/d9/0da86327df70349aa8d86390da91171bd3ca4f0e7c1d1d453a9c10344da3/regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434", size = 915066, upload-time = "2026-01-14T23:17:01.607Z" },
- { url = "https://files.pythonhosted.org/packages/2a/5e/f660fb23fc77baa2a61aa1f1fe3a4eea2bbb8a286ddec148030672e18834/regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a", size = 812938, upload-time = "2026-01-14T23:17:04.366Z" },
- { url = "https://files.pythonhosted.org/packages/69/33/a47a29bfecebbbfd1e5cd3f26b28020a97e4820f1c5148e66e3b7d4b4992/regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10", size = 781314, upload-time = "2026-01-14T23:17:06.378Z" },
- { url = "https://files.pythonhosted.org/packages/65/ec/7ec2bbfd4c3f4e494a24dec4c6943a668e2030426b1b8b949a6462d2c17b/regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac", size = 795652, upload-time = "2026-01-14T23:17:08.521Z" },
- { url = "https://files.pythonhosted.org/packages/46/79/a5d8651ae131fe27d7c521ad300aa7f1c7be1dbeee4d446498af5411b8a9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea", size = 868550, upload-time = "2026-01-14T23:17:10.573Z" },
- { url = "https://files.pythonhosted.org/packages/06/b7/25635d2809664b79f183070786a5552dd4e627e5aedb0065f4e3cf8ee37d/regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e", size = 769981, upload-time = "2026-01-14T23:17:12.871Z" },
- { url = "https://files.pythonhosted.org/packages/16/8b/fc3fcbb2393dcfa4a6c5ffad92dc498e842df4581ea9d14309fcd3c55fb9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521", size = 854780, upload-time = "2026-01-14T23:17:14.837Z" },
- { url = "https://files.pythonhosted.org/packages/d0/38/dde117c76c624713c8a2842530be9c93ca8b606c0f6102d86e8cd1ce8bea/regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db", size = 799778, upload-time = "2026-01-14T23:17:17.369Z" },
- { url = "https://files.pythonhosted.org/packages/e3/0d/3a6cfa9ae99606afb612d8fb7a66b245a9d5ff0f29bb347c8a30b6ad561b/regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e", size = 274667, upload-time = "2026-01-14T23:17:19.301Z" },
- { url = "https://files.pythonhosted.org/packages/5b/b2/297293bb0742fd06b8d8e2572db41a855cdf1cae0bf009b1cb74fe07e196/regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf", size = 284386, upload-time = "2026-01-14T23:17:21.231Z" },
- { url = "https://files.pythonhosted.org/packages/95/e4/a3b9480c78cf8ee86626cb06f8d931d74d775897d44201ccb813097ae697/regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70", size = 274837, upload-time = "2026-01-14T23:17:23.146Z" },
-]
-
-[[package]]
-name = "requests"
-version = "2.32.5"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "certifi" },
- { name = "charset-normalizer" },
- { name = "idna" },
- { name = "urllib3" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
-]
-
-[[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 = "rich"
-version = "14.3.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "markdown-it-py" },
- { name = "pygments" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/a1/84/4831f881aa6ff3c976f6d6809b58cdfa350593ffc0dc3c58f5f6586780fb/rich-14.3.1.tar.gz", hash = "sha256:b8c5f568a3a749f9290ec6bddedf835cec33696bfc1e48bcfecb276c7386e4b8", size = 230125, upload-time = "2026-01-24T21:40:44.847Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/87/2a/a1810c8627b9ec8c57ec5ec325d306701ae7be50235e8fd81266e002a3cc/rich-14.3.1-py3-none-any.whl", hash = "sha256:da750b1aebbff0b372557426fb3f35ba56de8ef954b3190315eb64076d6fb54e", size = 309952, upload-time = "2026-01-24T21:40:42.969Z" },
-]
-
-[[package]]
-name = "rsa"
-version = "4.9.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pyasn1" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
-]
-
-[[package]]
-name = "s3transfer"
-version = "0.16.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "botocore" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/05/04/74127fc843314818edfa81b5540e26dd537353b123a4edc563109d8f17dd/s3transfer-0.16.0.tar.gz", hash = "sha256:8e990f13268025792229cd52fa10cb7163744bf56e719e0b9cb925ab79abf920", size = 153827, upload-time = "2025-12-01T02:30:59.114Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/fc/51/727abb13f44c1fcf6d145979e1535a35794db0f6e450a0cb46aa24732fe2/s3transfer-0.16.0-py3-none-any.whl", hash = "sha256:18e25d66fed509e3868dc1572b3f427ff947dd2c56f844a5bf09481ad3f3b2fe", size = 86830, upload-time = "2025-12-01T02:30:57.729Z" },
-]
-
-[[package]]
-name = "shellingham"
-version = "1.5.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
-]
-
-[[package]]
-name = "six"
-version = "1.17.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
-]
-
-[[package]]
-name = "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 = "syrupy"
-version = "5.1.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pytest" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/2e/b0/24bca682d6a6337854be37f242d116cceeda9942571d5804c44bc1bdd427/syrupy-5.1.0.tar.gz", hash = "sha256:df543c7aa50d3cf1246e83d58fe490afe5f7dab7b41e74ecc0d8d23ae19bd4b8", size = 50495, upload-time = "2026-01-25T14:53:06.2Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/de/70/cf880c3b95a6034ef673e74b369941b42315c01f1554a5637a4f8b911009/syrupy-5.1.0-py3-none-any.whl", hash = "sha256:95162d2b05e61ed3e13f117b88dfab7c58bd6f90e66ebbf918e8a77114ad51c5", size = 51658, upload-time = "2026-01-25T14:53:05.105Z" },
-]
-
-[[package]]
-name = "tabulate"
-version = "0.9.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" },
-]
-
-[[package]]
-name = "tblib"
-version = "2.0.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/64/e3/d9aebe40d15d2c4c73a0ff8555326ef42a62ce3e5320ceb1aa762e4fbb54/tblib-2.0.0.tar.gz", hash = "sha256:a6df30f272c08bf8be66e0775fad862005d950a6b8449b94f7c788731d70ecd7", size = 28695, upload-time = "2023-06-22T08:24:16.494Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9e/bd/ccb241b97e39dd8ec143418f89b3fd5752f872c862877a0f1b2d9fb9e815/tblib-2.0.0-py3-none-any.whl", hash = "sha256:9100bfa016b047d5b980d66e7efed952fbd20bd85b56110aaf473cb97d18709a", size = 11455, upload-time = "2023-06-22T08:24:14.248Z" },
-]
-
-[[package]]
-name = "tenacity"
-version = "9.1.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036, upload-time = "2025-04-02T08:25:09.966Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248, upload-time = "2025-04-02T08:25:07.678Z" },
-]
-
-[[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 = "together"
-version = "1.5.35"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "aiohttp" },
- { name = "black" },
- { name = "click" },
- { name = "eval-type-backport" },
- { name = "filelock" },
- { name = "numpy" },
- { name = "pillow" },
- { name = "pydantic" },
- { name = "requests" },
- { name = "rich" },
- { name = "tabulate" },
- { name = "tqdm" },
- { name = "typer" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/5a/1d/6c50e0e32af097d966723e63b8e5ee02cfb002a40b6095c8ac65d6c08fe8/together-1.5.35.tar.gz", hash = "sha256:db3fc7dbc04dca044f437cd28224432e17567e6650dc1afd09780b48c0187cff", size = 91037, upload-time = "2026-01-21T23:15:15.909Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/68/e6/cd079d92f9eab83cd48c932e9b1e5e7fe25d90576f913dde66373135c392/together-1.5.35-py3-none-any.whl", hash = "sha256:74b6192e26492dbce2570fb801f884e74739bae1045b20c5b070a71639d7d5fc", size = 120461, upload-time = "2026-01-21T23:15:14.054Z" },
-]
-
-[[package]]
-name = "toml"
-version = "0.10.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" },
-]
-
-[[package]]
-name = "tomlkit"
-version = "0.14.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" },
-]
-
-[[package]]
-name = "tqdm"
-version = "4.67.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "colorama", marker = "sys_platform == 'win32'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
-]
-
-[[package]]
-name = "traceback-with-variables"
-version = "2.2.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/34/b1/25ee53be3125145cef9385f159a44547b4a01e1b2d2828055ca69b7e18aa/traceback_with_variables-2.2.1.tar.gz", hash = "sha256:ea7c695f9b401762f68f75df0439d661112b8dbd58bcd6910e402cff925ad7e0", size = 26145, upload-time = "2025-10-24T13:39:35.141Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/4a/06/fda9970d55fbbb7cd5cc856da2a9c693107e20f84386596da1f25b90a8cf/traceback_with_variables-2.2.1-py3-none-any.whl", hash = "sha256:ab6d75c72d26d61217962d11db44c98c62dccd2fedb2d4fb0ae4f9faf9db23c2", size = 22388, upload-time = "2025-10-24T13:29:32.712Z" },
-]
-
-[[package]]
-name = "typeid-python"
-version = "0.3.9"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "uuid-utils" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/ac/83/d1b140e4941a05ab7f4ccc2a5466b37fa559f48a9d3684d8107a7511508f/typeid_python-0.3.9.tar.gz", hash = "sha256:7cf7ede21e6ba8f272981dbae504d1256261d03edab42fb05d36787dbfd589bd", size = 25201, upload-time = "2026-01-28T18:23:38.917Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/5b/30/523b728eab3157d818818ac022579b37b2d5f7994eb6e2bb9636a08a712a/typeid_python-0.3.9-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c37992e3aeee2ca2d1c5412a8a1bbbff71454c7db30b343a40ac2ab7ffe0d892", size = 241454, upload-time = "2026-01-28T18:23:14.452Z" },
- { url = "https://files.pythonhosted.org/packages/e0/da/13d577f76f5fceb77dbdc94b8435d86fbbbe472d5fa86245a2ece22ece20/typeid_python-0.3.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b72f9e75eb8ae229e56c00a25330fbb9881f76f9b3a67fd3e35470328db1078", size = 238088, upload-time = "2026-01-28T18:23:16.429Z" },
- { url = "https://files.pythonhosted.org/packages/4d/36/a81e397a341d51315288835a809c5038835f925da00d11dce3e10115693f/typeid_python-0.3.9-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:b035ef63c243776783551963983d12146b134c52475edfc42a33ce0a9baea337", size = 271957, upload-time = "2026-01-28T18:23:18.236Z" },
- { url = "https://files.pythonhosted.org/packages/03/06/dd85db0de10c6337e3e9fc457fda009f4cdac83fa873418c7e19c6041ecb/typeid_python-0.3.9-cp311-cp311-win_amd64.whl", hash = "sha256:751b0aaeeb5c8c0bd87d15c77ffe52df86c372d66572e6b2ec3fb2df056d790e", size = 132071, upload-time = "2026-01-28T18:23:19.583Z" },
- { url = "https://files.pythonhosted.org/packages/6c/7b/bb76c6016f862b9cb5a4e7d6cb3d1378cc342793644ba6a2022f05790dc6/typeid_python-0.3.9-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:fc193d8a17529c65915273a9734e7d724b8b279450d01b2559a77b8bd53f52da", size = 240430, upload-time = "2026-01-28T18:23:21.506Z" },
- { url = "https://files.pythonhosted.org/packages/20/0c/f8834a0d465afc4ea194d43fe10dd11e23874f5c102cec260172108a5cdc/typeid_python-0.3.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7dedb9894dea2a39653822224af6292710a54af0c9f48ba896bea6c22b6bff06", size = 236578, upload-time = "2026-01-28T18:23:22.874Z" },
- { url = "https://files.pythonhosted.org/packages/b1/29/c9842c47610213821cddbb274d093e36dc5d4346195eb5f036856f7d15f3/typeid_python-0.3.9-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:20ce022fe7d6be11a0d332d794e75a056b5aa225969f4b8ef9dbe4a94e651f2c", size = 270059, upload-time = "2026-01-28T18:23:24.396Z" },
- { url = "https://files.pythonhosted.org/packages/8b/87/9c3ae9936491f29ff09dcdbfdc4476368cc75b09be515ab31c5a2519ea7d/typeid_python-0.3.9-cp312-cp312-win_amd64.whl", hash = "sha256:bba27b8d1708e9654b85ffbcc4fc89a7c1d6cea65fbdab7fb9069d8cd71c2acd", size = 131062, upload-time = "2026-01-28T18:23:25.832Z" },
- { url = "https://files.pythonhosted.org/packages/37/d8/366968632ad71e09125761f5313289e001695424ae68847e5f6df1e22b7b/typeid_python-0.3.9-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:11521763da6308ce0119d5a145c51b0e9d22b04b93e151c3ba73d5a359ec714b", size = 240529, upload-time = "2026-01-28T18:23:27.555Z" },
- { url = "https://files.pythonhosted.org/packages/3c/43/dd71c7d5db5ca1bcf0f4c10771c45792655a4b4552cc4482d10a9fbc3838/typeid_python-0.3.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c8e32ba0b0da2f7520bc2d2b8301dfe56dc1a1094405751eb8a24d9a8b59c4a", size = 236828, upload-time = "2026-01-28T18:23:29.288Z" },
- { url = "https://files.pythonhosted.org/packages/cb/31/778a97d5f6dd0f0c4c8dd469a77be2bc8006ff76521bb27146a284ec0757/typeid_python-0.3.9-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:22c617e05319768e3cc8e0b4750238d4b1b5359746f0a0293e06dc4cd298bc80", size = 269743, upload-time = "2026-01-28T18:23:31.022Z" },
- { url = "https://files.pythonhosted.org/packages/d5/55/9891583b35aff55c9161005de8eb1444f928f1a4718e5a76fd8d86ec292d/typeid_python-0.3.9-cp313-cp313-win_amd64.whl", hash = "sha256:630cbcb17f3ac81ae346959cdf1ee7e69204e467bbfbf577fb6d98f38c769468", size = 130958, upload-time = "2026-01-28T18:23:32.358Z" },
- { url = "https://files.pythonhosted.org/packages/67/00/8c53af3d3859aeac99a6907018dfc118191bb05e08be4e4f2ddcab2cf5f6/typeid_python-0.3.9-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:4b88012e1ba62933f1b4161c0fb22e80b1047a45391cbe6ef1580ffebf95ed0a", size = 240337, upload-time = "2026-01-28T18:23:33.53Z" },
- { url = "https://files.pythonhosted.org/packages/ca/90/e451b06da5c28dc3ba7f08bd5d2cbe5dbd4b25e79c20eb60641ebb42c2c6/typeid_python-0.3.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:54e15462494f04c5bb3ea396ff65b3e1820eb80e9ffcd70575c3b2055dc4d3e9", size = 236451, upload-time = "2026-01-28T18:23:35.001Z" },
- { url = "https://files.pythonhosted.org/packages/87/fd/3929b7ce31298cd2ae716e6e991a248e24641e483f9d66b16755939d83ff/typeid_python-0.3.9-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:f2ae659428e88f382fadfb3801f9aa0d4c0bec5a67205570ef95ce0ca81ed5b8", size = 269235, upload-time = "2026-01-28T18:23:36.419Z" },
- { url = "https://files.pythonhosted.org/packages/cd/56/f1548a9f0c9c71a8d67150f50bc3a2ef36a74b89f060ab90685ef07ea859/typeid_python-0.3.9-cp314-cp314-win_amd64.whl", hash = "sha256:1fc43c61b228c281c099e7d68824a75202dd81427cdb4f411b93383d8350c428", size = 130948, upload-time = "2026-01-28T18:23:37.705Z" },
-]
-
-[[package]]
-name = "typer"
-version = "0.19.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "click" },
- { name = "rich" },
- { name = "shellingham" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/21/ca/950278884e2ca20547ff3eb109478c6baf6b8cf219318e6bc4f666fad8e8/typer-0.19.2.tar.gz", hash = "sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca", size = 104755, upload-time = "2025-09-23T09:47:48.256Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/00/22/35617eee79080a5d071d0f14ad698d325ee6b3bf824fc0467c03b30e7fa8/typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9", size = 46748, upload-time = "2025-09-23T09:47:46.777Z" },
-]
-
-[[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 = "tzdata"
-version = "2025.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
-]
-
-[[package]]
-name = "urllib3"
-version = "2.6.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
-]
-
-[[package]]
-name = "uuid-utils"
-version = "0.14.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/57/7c/3a926e847516e67bc6838634f2e54e24381105b4e80f9338dc35cca0086b/uuid_utils-0.14.0.tar.gz", hash = "sha256:fc5bac21e9933ea6c590433c11aa54aaca599f690c08069e364eb13a12f670b4", size = 22072, upload-time = "2026-01-20T20:37:15.729Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a7/42/42d003f4a99ddc901eef2fd41acb3694163835e037fb6dde79ad68a72342/uuid_utils-0.14.0-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f6695c0bed8b18a904321e115afe73b34444bc8451d0ce3244a1ec3b84deb0e5", size = 601786, upload-time = "2026-01-20T20:37:09.843Z" },
- { url = "https://files.pythonhosted.org/packages/96/e6/775dfb91f74b18f7207e3201eb31ee666d286579990dc69dd50db2d92813/uuid_utils-0.14.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:4f0a730bbf2d8bb2c11b93e1005e91769f2f533fa1125ed1f00fd15b6fcc732b", size = 303943, upload-time = "2026-01-20T20:37:18.767Z" },
- { url = "https://files.pythonhosted.org/packages/17/82/ea5f5e85560b08a1f30cdc65f75e76494dc7aba9773f679e7eaa27370229/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40ce3fd1a4fdedae618fc3edc8faf91897012469169d600133470f49fd699ed3", size = 340467, upload-time = "2026-01-20T20:37:11.794Z" },
- { url = "https://files.pythonhosted.org/packages/ca/33/54b06415767f4569882e99b6470c6c8eeb97422686a6d432464f9967fd91/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:09ae4a98416a440e78f7d9543d11b11cae4bab538b7ed94ec5da5221481748f2", size = 346333, upload-time = "2026-01-20T20:37:12.818Z" },
- { url = "https://files.pythonhosted.org/packages/cb/10/a6bce636b8f95e65dc84bf4a58ce8205b8e0a2a300a38cdbc83a3f763d27/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:971e8c26b90d8ae727e7f2ac3ee23e265971d448b3672882f2eb44828b2b8c3e", size = 470859, upload-time = "2026-01-20T20:37:01.512Z" },
- { url = "https://files.pythonhosted.org/packages/8a/27/84121c51ea72f013f0e03d0886bcdfa96b31c9b83c98300a7bd5cc4fa191/uuid_utils-0.14.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5cde1fa82804a8f9d2907b7aec2009d440062c63f04abbdb825fce717a5e860", size = 341988, upload-time = "2026-01-20T20:37:22.881Z" },
- { url = "https://files.pythonhosted.org/packages/90/a4/01c1c7af5e6a44f20b40183e8dac37d6ed83e7dc9e8df85370a15959b804/uuid_utils-0.14.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7343862a2359e0bd48a7f3dfb5105877a1728677818bb694d9f40703264a2db", size = 365784, upload-time = "2026-01-20T20:37:10.808Z" },
- { url = "https://files.pythonhosted.org/packages/04/f0/65ee43ec617b8b6b1bf2a5aecd56a069a08cca3d9340c1de86024331bde3/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c51e4818fdb08ccec12dc7083a01f49507b4608770a0ab22368001685d59381b", size = 523750, upload-time = "2026-01-20T20:37:06.152Z" },
- { url = "https://files.pythonhosted.org/packages/95/d3/6bf503e3f135a5dfe705a65e6f89f19bccd55ac3fb16cb5d3ec5ba5388b8/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:181bbcccb6f93d80a8504b5bd47b311a1c31395139596edbc47b154b0685b533", size = 615818, upload-time = "2026-01-20T20:37:21.816Z" },
- { url = "https://files.pythonhosted.org/packages/df/6c/99937dd78d07f73bba831c8dc9469dfe4696539eba2fc269ae1b92752f9e/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:5c8ae96101c3524ba8dbf762b6f05e9e9d896544786c503a727c5bf5cb9af1a7", size = 580831, upload-time = "2026-01-20T20:37:19.691Z" },
- { url = "https://files.pythonhosted.org/packages/44/fa/bbc9e2c25abd09a293b9b097a0d8fc16acd6a92854f0ec080f1ea7ad8bb3/uuid_utils-0.14.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:00ac3c6edfdaff7e1eed041f4800ae09a3361287be780d7610a90fdcde9befdc", size = 546333, upload-time = "2026-01-20T20:37:03.117Z" },
- { url = "https://files.pythonhosted.org/packages/e7/9b/e5e99b324b1b5f0c62882230455786df0bc66f67eff3b452447e703f45d2/uuid_utils-0.14.0-cp39-abi3-win32.whl", hash = "sha256:ec2fd80adf8e0e6589d40699e6f6df94c93edcc16dd999be0438dd007c77b151", size = 177319, upload-time = "2026-01-20T20:37:04.208Z" },
- { url = "https://files.pythonhosted.org/packages/d3/28/2c7d417ea483b6ff7820c948678fdf2ac98899dc7e43bb15852faa95acaf/uuid_utils-0.14.0-cp39-abi3-win_amd64.whl", hash = "sha256:efe881eb43a5504fad922644cb93d725fd8a6a6d949bd5a4b4b7d1a1587c7fd1", size = 182566, upload-time = "2026-01-20T20:37:16.868Z" },
- { url = "https://files.pythonhosted.org/packages/b8/86/49e4bdda28e962fbd7266684171ee29b3d92019116971d58783e51770745/uuid_utils-0.14.0-cp39-abi3-win_arm64.whl", hash = "sha256:32b372b8fd4ebd44d3a219e093fe981af4afdeda2994ee7db208ab065cfcd080", size = 182809, upload-time = "2026-01-20T20:37:05.139Z" },
- { url = "https://files.pythonhosted.org/packages/f1/03/1f1146e32e94d1f260dfabc81e1649102083303fb4ad549775c943425d9a/uuid_utils-0.14.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:762e8d67992ac4d2454e24a141a1c82142b5bde10409818c62adbe9924ebc86d", size = 587430, upload-time = "2026-01-20T20:37:24.998Z" },
- { url = "https://files.pythonhosted.org/packages/87/ba/d5a7469362594d885fd9219fe9e851efbe65101d3ef1ef25ea321d7ce841/uuid_utils-0.14.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:40be5bf0b13aa849d9062abc86c198be6a25ff35316ce0b89fc25f3bac6d525e", size = 298106, upload-time = "2026-01-20T20:37:23.896Z" },
- { url = "https://files.pythonhosted.org/packages/8a/11/3dafb2a5502586f59fd49e93f5802cd5face82921b3a0f3abb5f357cb879/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:191a90a6f3940d1b7322b6e6cceff4dd533c943659e0a15f788674407856a515", size = 333423, upload-time = "2026-01-20T20:37:17.828Z" },
- { url = "https://files.pythonhosted.org/packages/7c/f2/c8987663f0cdcf4d717a36d85b5db2a5589df0a4e129aa10f16f4380ef48/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4aa4525f4ad82f9d9c842f9a3703f1539c1808affbaec07bb1b842f6b8b96aa5", size = 338659, upload-time = "2026-01-20T20:37:14.286Z" },
- { url = "https://files.pythonhosted.org/packages/d1/c8/929d81665d83f0b2ffaecb8e66c3091a50f62c7cb5b65e678bd75a96684e/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cdbd82ff20147461caefc375551595ecf77ebb384e46267f128aca45a0f2cdfc", size = 467029, upload-time = "2026-01-20T20:37:08.277Z" },
- { url = "https://files.pythonhosted.org/packages/8e/a0/27d7daa1bfed7163f4ccaf52d7d2f4ad7bb1002a85b45077938b91ee584f/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff57e8a5d540006ce73cf0841a643d445afe78ba12e75ac53a95ca2924a56be", size = 333298, upload-time = "2026-01-20T20:37:07.271Z" },
- { url = "https://files.pythonhosted.org/packages/63/d4/acad86ce012b42ce18a12f31ee2aa3cbeeb98664f865f05f68c882945913/uuid_utils-0.14.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3fd9112ca96978361201e669729784f26c71fecc9c13a7f8a07162c31bd4d1e2", size = 359217, upload-time = "2026-01-20T20:36:59.687Z" },
-]
-
-[[package]]
-name = "vet"
-version = "0.1.0"
-source = { editable = "../" }
-dependencies = [
- { name = "aiohttp" },
- { name = "click" },
- { name = "imbue-core" },
- { name = "imbue-tools" },
- { name = "jinja2" },
- { name = "loguru" },
- { name = "pydantic" },
- { name = "pygments" },
- { name = "pytest" },
- { name = "syrupy" },
- { name = "together" },
- { name = "vet-types" },
-]
-
-[package.metadata]
-requires-dist = [
- { name = "aiohttp", specifier = ">=3.8.0" },
- { name = "click" },
- { name = "imbue-core", editable = "../imbue_core" },
- { name = "imbue-tools", editable = "." },
- { name = "jinja2" },
- { name = "loguru" },
- { name = "pydantic" },
- { name = "pygments", specifier = ">=2.0.0" },
- { name = "pytest" },
- { name = "syrupy" },
- { name = "together", specifier = ">=1.5.35" },
- { name = "vet-types", editable = "../vet_types" },
-]
-
-[package.metadata.requires-dev]
-dev = [{ name = "black" }]
-
-[[package]]
-name = "vet-types"
-version = "0.1.0"
-source = { editable = "../vet_types" }
-dependencies = [
- { name = "imbue-core" },
- { name = "pydantic" },
- { name = "typeid-python" },
-]
-
-[package.metadata]
-requires-dist = [
- { name = "imbue-core" },
- { name = "pydantic" },
- { name = "typeid-python" },
-]
-
-[[package]]
-name = "websockets"
-version = "15.0.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" },
- { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" },
- { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" },
- { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" },
- { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" },
- { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" },
- { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" },
- { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" },
- { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" },
- { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" },
- { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" },
- { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" },
- { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" },
- { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" },
- { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" },
- { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" },
- { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" },
- { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" },
- { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" },
- { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" },
- { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" },
- { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" },
- { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
- { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
- { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
- { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
- { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
- { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
- { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
- { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
- { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
- { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
- { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
- { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
-]
-
-[[package]]
-name = "win32-setctime"
-version = "1.2.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" },
-]
-
-[[package]]
-name = "yarl"
-version = "1.22.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "idna" },
- { name = "multidict" },
- { name = "propcache" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" },
- { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" },
- { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" },
- { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" },
- { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" },
- { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" },
- { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" },
- { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" },
- { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" },
- { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" },
- { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" },
- { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" },
- { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" },
- { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" },
- { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" },
- { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" },
- { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" },
- { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" },
- { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" },
- { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" },
- { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" },
- { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" },
- { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" },
- { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" },
- { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" },
- { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" },
- { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" },
- { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" },
- { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" },
- { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" },
- { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" },
- { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" },
- { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" },
- { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" },
- { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" },
- { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" },
- { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" },
- { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" },
- { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" },
- { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" },
- { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" },
- { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" },
- { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" },
- { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" },
- { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" },
- { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" },
- { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" },
- { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" },
- { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" },
- { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" },
- { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" },
- { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" },
- { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" },
- { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" },
- { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" },
- { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" },
- { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" },
- { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" },
- { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" },
- { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" },
- { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" },
- { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" },
- { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" },
- { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" },
- { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" },
- { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" },
- { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" },
- { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" },
- { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" },
- { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" },
- { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" },
- { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" },
- { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" },
- { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" },
- { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" },
- { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" },
- { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" },
- { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" },
- { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" },
- { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" },
- { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" },
- { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" },
- { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" },
- { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" },
- { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" },
- { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" },
- { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" },
- { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" },
- { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" },
- { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" },
- { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" },
- { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" },
- { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" },
- { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" },
- { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" },
- { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" },
- { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" },
-]
-
-[[package]]
-name = "yasoo"
-version = "0.12.6"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "attrs" },
- { name = "more-itertools" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/cf/00/a1ed9035b00254227c684161c3d4037f767ed53a1993b69c00c9d4d94f25/yasoo-0.12.6.tar.gz", hash = "sha256:aec81e790045198e8f51f92353f11923580f1c94f49eed2b543f286e4cc1c5cc", size = 13705, upload-time = "2022-10-22T10:05:39.619Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/68/21/cfd73fd9cc69855bffdda55cbeefa45ffd415f97704f67ceda1eb57141ad/yasoo-0.12.6-py3-none-any.whl", hash = "sha256:7400ff055c2153d670c04c82621fd4aeafc3f1fc7c0f99240831752357b67e1f", size = 14850, upload-time = "2022-10-22T10:05:38.4Z" },
-]
diff --git a/pyproject.toml b/pyproject.toml
@@ -7,20 +7,55 @@ name = "vet"
version = "0.1.0"
readme = "README.md"
dependencies = [
+ # Original vet dependencies
"aiohttp>=3.8.0",
"click",
- "imbue_core",
- "imbue_tools",
"jinja2",
"loguru",
- "pydantic",
+ "pydantic>=2.11.4",
"pygments>=2.0.0",
"pytest",
"syrupy",
"together>=1.5.35",
- "vet_types",
+
+ # From imbue_core
+ "anyio",
+ "attrs",
+ "boto3>=1.38.27",
+ "cachetools",
+ "cattrs",
+ "diskcache>=5.6.3",
+ "grpclib>=0.4.7",
+ "httpx",
+ "inline-snapshot",
+ "pathspec",
+ "prometheus-client>=0.20.0",
+ "pydantic-settings",
+ "pygit2>=1.18.0",
+ "pylint==3.2.6",
+ "pyhumps",
+ "pytest-asyncio",
+ "pytest-mock",
+ "python-gitlab>=4.5.0",
+ "tblib==2.0.0",
+ "tenacity>=8.2.2",
+ "toml",
+ "traceback-with-variables>=2.2.0",
+ "typeid-python",
+ "yasoo",
+ "anthropic~=0.54",
+ "openai>=1.79.0",
+ "tiktoken",
+ "groq>=0.18.0",
+ "google-genai>=1.26.0",
+
+ # From imbue_tools
+ "async_lru",
+ "libcst",
+ "psycopg[binary]",
+ "requests",
]
-requires-python = ">=3.11"
+requires-python = ">=3.11,<3.13"
[project.scripts]
vet = "vet.cli.main:main"
@@ -31,14 +66,10 @@ package-data.vet = ["py.typed"]
[tool.setuptools.packages.find]
include = ["vet*"]
-[tool.uv.sources]
-imbue_core = { path = "./imbue_core", editable = true }
-imbue_tools = { path = "./imbue_tools", editable = true }
-vet_types = { path = "./vet_types", editable = true }
-
[dependency-groups]
dev = [
"black",
+ "moto>=4.1.12",
]
[tool.black]
diff --git a/uv.lock b/uv.lock
@@ -1,10 +1,8 @@
version = 1
revision = 3
-requires-python = ">=3.11"
+requires-python = ">=3.11, <3.13"
resolution-markers = [
- "python_full_version >= '3.14'",
- "python_full_version == '3.13.*'",
- "python_full_version == '3.12.*'",
+ "python_full_version >= '3.12'",
"python_full_version < '3.12'",
]
@@ -66,57 +64,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" },
{ url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" },
{ url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" },
- { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" },
- { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" },
- { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" },
- { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" },
- { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" },
- { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" },
- { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" },
- { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" },
- { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" },
- { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" },
- { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" },
- { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" },
- { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" },
- { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" },
- { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" },
- { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" },
- { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" },
- { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" },
- { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" },
- { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" },
- { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" },
- { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" },
- { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" },
- { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" },
- { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" },
- { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" },
- { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" },
- { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" },
- { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" },
- { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" },
- { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" },
- { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" },
- { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" },
- { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" },
- { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" },
- { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" },
- { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" },
- { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" },
- { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" },
- { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" },
- { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" },
- { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" },
- { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" },
- { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" },
- { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" },
- { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" },
- { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" },
- { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" },
- { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" },
- { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" },
- { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" },
]
[[package]]
@@ -125,7 +72,7 @@ version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "frozenlist" },
- { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+ { name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" }
wheels = [
@@ -143,7 +90,7 @@ wheels = [
[[package]]
name = "anthropic"
-version = "0.76.0"
+version = "0.77.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -155,9 +102,9 @@ dependencies = [
{ name = "sniffio" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/6e/be/d11abafaa15d6304826438170f7574d750218f49a106c54424a40cef4494/anthropic-0.76.0.tar.gz", hash = "sha256:e0cae6a368986d5cf6df743dfbb1b9519e6a9eee9c6c942ad8121c0b34416ffe", size = 495483, upload-time = "2026-01-13T18:41:14.908Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/eb/85/6cb5da3cf91de2eeea89726316e8c5c8c31e2d61ee7cb1233d7e95512c31/anthropic-0.77.0.tar.gz", hash = "sha256:ce36efeb80cb1e25430a88440dc0f9aa5c87f10d080ab70a1bdfd5c2c5fbedb4", size = 504575, upload-time = "2026-01-29T18:20:41.507Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e5/70/7b0fd9c1a738f59d3babe2b4212031c34ab7d0fda4ffef15b58a55c5bcea/anthropic-0.76.0-py3-none-any.whl", hash = "sha256:81efa3113901192af2f0fe977d3ec73fdadb1e691586306c4256cd6d5ccc331c", size = 390309, upload-time = "2026-01-13T18:41:13.483Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/27/9df785d3f94df9ac72f43ee9e14b8120b37d992b18f4952774ed46145022/anthropic-0.77.0-py3-none-any.whl", hash = "sha256:65cc83a3c82ce622d5c677d0d7706c77d29dc83958c6b10286e12fda6ffb2651", size = 397867, upload-time = "2026-01-29T18:20:39.481Z" },
]
[[package]]
@@ -166,7 +113,7 @@ version = "4.12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
- { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+ { name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
wheels = [
@@ -233,54 +180,44 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/f3/360fa4182e36e9875fabcf3a9717db9d27a8d11870f21cff97725c54f35b/black-25.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1f68c5eff61f226934be6b5b80296cf6939e5d2f0c2f7d543ea08b204bfaf59", size = 1800158, upload-time = "2025-12-08T01:44:27.301Z" },
{ url = "https://files.pythonhosted.org/packages/f8/08/2c64830cb6616278067e040acca21d4f79727b23077633953081c9445d61/black-25.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:274f940c147ddab4442d316b27f9e332ca586d39c85ecf59ebdea82cc9ee8892", size = 1426197, upload-time = "2025-12-08T01:45:51.198Z" },
{ url = "https://files.pythonhosted.org/packages/d4/60/a93f55fd9b9816b7432cf6842f0e3000fdd5b7869492a04b9011a133ee37/black-25.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:169506ba91ef21e2e0591563deda7f00030cb466e747c4b09cb0a9dae5db2f43", size = 1237266, upload-time = "2025-12-08T01:45:10.556Z" },
- { url = "https://files.pythonhosted.org/packages/c8/52/c551e36bc95495d2aa1a37d50566267aa47608c81a53f91daa809e03293f/black-25.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a05ddeb656534c3e27a05a29196c962877c83fa5503db89e68857d1161ad08a5", size = 1923809, upload-time = "2025-12-08T01:46:55.126Z" },
- { url = "https://files.pythonhosted.org/packages/a0/f7/aac9b014140ee56d247e707af8db0aae2e9efc28d4a8aba92d0abd7ae9d1/black-25.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ec77439ef3e34896995503865a85732c94396edcc739f302c5673a2315e1e7f", size = 1742384, upload-time = "2025-12-08T01:49:37.022Z" },
- { url = "https://files.pythonhosted.org/packages/74/98/38aaa018b2ab06a863974c12b14a6266badc192b20603a81b738c47e902e/black-25.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e509c858adf63aa61d908061b52e580c40eae0dfa72415fa47ac01b12e29baf", size = 1798761, upload-time = "2025-12-08T01:46:05.386Z" },
- { url = "https://files.pythonhosted.org/packages/16/3a/a8ac542125f61574a3f015b521ca83b47321ed19bb63fe6d7560f348bfe1/black-25.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:252678f07f5bac4ff0d0e9b261fbb029fa530cfa206d0a636a34ab445ef8ca9d", size = 1429180, upload-time = "2025-12-08T01:45:34.903Z" },
- { url = "https://files.pythonhosted.org/packages/e6/2d/bdc466a3db9145e946762d52cd55b1385509d9f9004fec1c97bdc8debbfb/black-25.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc5b1c09fe3c931ddd20ee548511c64ebf964ada7e6f0763d443947fd1c603ce", size = 1239350, upload-time = "2025-12-08T01:46:09.458Z" },
- { url = "https://files.pythonhosted.org/packages/35/46/1d8f2542210c502e2ae1060b2e09e47af6a5e5963cb78e22ec1a11170b28/black-25.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0a0953b134f9335c2434864a643c842c44fba562155c738a2a37a4d61f00cad5", size = 1917015, upload-time = "2025-12-08T01:53:27.987Z" },
- { url = "https://files.pythonhosted.org/packages/41/37/68accadf977672beb8e2c64e080f568c74159c1aaa6414b4cd2aef2d7906/black-25.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2355bbb6c3b76062870942d8cc450d4f8ac71f9c93c40122762c8784df49543f", size = 1741830, upload-time = "2025-12-08T01:54:36.861Z" },
- { url = "https://files.pythonhosted.org/packages/ac/76/03608a9d8f0faad47a3af3a3c8c53af3367f6c0dd2d23a84710456c7ac56/black-25.12.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9678bd991cc793e81d19aeeae57966ee02909877cb65838ccffef24c3ebac08f", size = 1791450, upload-time = "2025-12-08T01:44:52.581Z" },
- { url = "https://files.pythonhosted.org/packages/06/99/b2a4bd7dfaea7964974f947e1c76d6886d65fe5d24f687df2d85406b2609/black-25.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:97596189949a8aad13ad12fcbb4ae89330039b96ad6742e6f6b45e75ad5cfd83", size = 1452042, upload-time = "2025-12-08T01:46:13.188Z" },
- { url = "https://files.pythonhosted.org/packages/b2/7c/d9825de75ae5dd7795d007681b752275ea85a1c5d83269b4b9c754c2aaab/black-25.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:778285d9ea197f34704e3791ea9404cd6d07595745907dd2ce3da7a13627b29b", size = 1267446, upload-time = "2025-12-08T01:46:14.497Z" },
{ url = "https://files.pythonhosted.org/packages/68/11/21331aed19145a952ad28fca2756a1433ee9308079bd03bd898e903a2e53/black-25.12.0-py3-none-any.whl", hash = "sha256:48ceb36c16dbc84062740049eef990bb2ce07598272e673c17d1a7720c71c828", size = 206191, upload-time = "2025-12-08T01:40:50.963Z" },
]
[[package]]
name = "boto3"
-version = "1.42.36"
+version = "1.42.39"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "botocore" },
{ name = "jmespath" },
{ name = "s3transfer" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/54/06/50ff808cf4f40efada8edc20f9d563ab287864423c874dfb94f755a60c52/boto3-1.42.36.tar.gz", hash = "sha256:a4eb51105c8c5d7b2bc2a9e2316e69baf69a55611275b9f189c0cf59f1aae171", size = 112839, upload-time = "2026-01-27T20:38:26.992Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/b8/ea/b96c77da49fed28744ee0347374d8223994a2b8570e76e8380a4064a8c4a/boto3-1.42.39.tar.gz", hash = "sha256:d03f82363314759eff7f84a27b9e6428125f89d8119e4588e8c2c1d79892c956", size = 112783, upload-time = "2026-01-30T20:38:31.226Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/8d/d1/35d12f04a7792e2f5e9ddff3c7b60493a32027761380dee7f24ee8ae80cc/boto3-1.42.36-py3-none-any.whl", hash = "sha256:e0ff6f2747bfdec63405b35ea185a7aea35239c3f4fe99e4d29368a6de9c4a84", size = 140604, upload-time = "2026-01-27T20:38:25.349Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/c4/3493b5c86e32d6dd558b30d16b55503e24a6e6cd7115714bc102b247d26e/boto3-1.42.39-py3-none-any.whl", hash = "sha256:d9d6ce11df309707b490d2f5f785b761cfddfd6d1f665385b78c9d8ed097184b", size = 140606, upload-time = "2026-01-30T20:38:28.635Z" },
]
[[package]]
name = "botocore"
-version = "1.42.36"
+version = "1.42.39"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jmespath" },
{ name = "python-dateutil" },
{ name = "urllib3" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/66/4e/b24089cf7a77d38886ac4fbae300a3c4c6d68c1b9ccb66af03cb07b6c35c/botocore-1.42.36.tar.gz", hash = "sha256:2ebd89cc75927944e2cee51b7adce749f38e0cb269a758a6464a27f8bcca65fb", size = 14909073, upload-time = "2026-01-27T20:38:16.621Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/ac/a6/3a34d1b74effc0f759f5ff4e91c77729d932bc34dd3207905e9ecbba1103/botocore-1.42.39.tar.gz", hash = "sha256:0f00355050821e91a5fe6d932f7bf220f337249b752899e3e4cf6ed54326249e", size = 14914927, upload-time = "2026-01-30T20:38:19.265Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e6/e8/f14d25bd768187424b385bc6a44e2ed5e96964e461ba019add03e48713c7/botocore-1.42.36-py3-none-any.whl", hash = "sha256:2cfae4c482e5e87bd835ab4289b711490c161ba57e852c06b65a03e7c25e08eb", size = 14583066, upload-time = "2026-01-27T20:38:14.02Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/71/9a2c88abb5fe47b46168b262254d5b5d635de371eba4bd01ea5c8c109575/botocore-1.42.39-py3-none-any.whl", hash = "sha256:9e0d0fed9226449cc26fcf2bbffc0392ac698dd8378e8395ce54f3ec13f81d58", size = 14591958, upload-time = "2026-01-30T20:38:14.814Z" },
]
[[package]]
name = "cachetools"
-version = "6.2.6"
+version = "7.0.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/39/91/d9ae9a66b01102a18cd16db0cf4cd54187ffe10f0865cc80071a4104fbb3/cachetools-6.2.6.tar.gz", hash = "sha256:16c33e1f276b9a9c0b49ab5782d901e3ad3de0dd6da9bf9bcd29ac5672f2f9e6", size = 32363, upload-time = "2026-01-27T20:32:59.956Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/98/af/df70e9b65bc77a1cbe0768c0aa4617147f30f8306ded98c1744bcdc0ae1e/cachetools-7.0.0.tar.gz", hash = "sha256:a9abf18ff3b86c7d05b27ead412e235e16ae045925e531fae38d5fada5ed5b08", size = 35796, upload-time = "2026-02-01T18:59:47.411Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/90/45/f458fa2c388e79dd9d8b9b0c99f1d31b568f27388f2fdba7bb66bbc0c6ed/cachetools-6.2.6-py3-none-any.whl", hash = "sha256:8c9717235b3c651603fff0076db52d6acbfd1b338b8ed50256092f7ce9c85bda", size = 11668, upload-time = "2026-01-27T20:32:58.527Z" },
+ { url = "https://files.pythonhosted.org/packages/28/df/2dd32cce20cbcf6f2ec456b58d44368161ad28320729f64e5e1d5d7bd0ae/cachetools-7.0.0-py3-none-any.whl", hash = "sha256:d52fef60e6e964a1969cfb61ccf6242a801b432790fe520d78720d757c81cbd2", size = 13487, upload-time = "2026-02-01T18:59:45.981Z" },
]
[[package]]
@@ -339,40 +276,6 @@ wheels = [
{ 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]]
@@ -413,38 +316,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
{ url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
{ url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
- { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
- { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
- { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
- { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
- { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
- { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
- { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
- { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
- { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
- { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
- { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
- { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
- { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
- { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
- { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
- { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
- { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
- { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
- { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
- { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
- { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
- { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
- { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
- { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
- { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
- { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
- { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
- { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
- { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
- { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
- { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
- { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
]
@@ -492,20 +363,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/f7/7923886f32dc47e27adeff8246e976d77258fd2aa3efdd1754e4e323bf49/cryptography-46.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2d08bc22efd73e8854b0b7caff402d735b354862f1145d7be3b9c0f740fef6a0", size = 4666059, upload-time = "2026-01-28T00:23:26.766Z" },
{ url = "https://files.pythonhosted.org/packages/eb/a7/0fca0fd3591dffc297278a61813d7f661a14243dd60f499a7a5b48acb52a/cryptography-46.0.4-cp311-abi3-win32.whl", hash = "sha256:82a62483daf20b8134f6e92898da70d04d0ef9a75829d732ea1018678185f4f5", size = 3026378, upload-time = "2026-01-28T00:23:28.317Z" },
{ url = "https://files.pythonhosted.org/packages/2d/12/652c84b6f9873f0909374864a57b003686c642ea48c84d6c7e2c515e6da5/cryptography-46.0.4-cp311-abi3-win_amd64.whl", hash = "sha256:6225d3ebe26a55dbc8ead5ad1265c0403552a63336499564675b29eb3184c09b", size = 3478614, upload-time = "2026-01-28T00:23:30.275Z" },
- { url = "https://files.pythonhosted.org/packages/b9/27/542b029f293a5cce59349d799d4d8484b3b1654a7b9a0585c266e974a488/cryptography-46.0.4-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:485e2b65d25ec0d901bca7bcae0f53b00133bf3173916d8e421f6fddde103908", size = 7116417, upload-time = "2026-01-28T00:23:31.958Z" },
- { url = "https://files.pythonhosted.org/packages/f8/f5/559c25b77f40b6bf828eabaf988efb8b0e17b573545edb503368ca0a2a03/cryptography-46.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:078e5f06bd2fa5aea5a324f2a09f914b1484f1d0c2a4d6a8a28c74e72f65f2da", size = 4264508, upload-time = "2026-01-28T00:23:34.264Z" },
- { url = "https://files.pythonhosted.org/packages/49/a1/551fa162d33074b660dc35c9bc3616fefa21a0e8c1edd27b92559902e408/cryptography-46.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dce1e4f068f03008da7fa51cc7abc6ddc5e5de3e3d1550334eaf8393982a5829", size = 4409080, upload-time = "2026-01-28T00:23:35.793Z" },
- { url = "https://files.pythonhosted.org/packages/b0/6a/4d8d129a755f5d6df1bbee69ea2f35ebfa954fa1847690d1db2e8bca46a5/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:2067461c80271f422ee7bdbe79b9b4be54a5162e90345f86a23445a0cf3fd8a2", size = 4270039, upload-time = "2026-01-28T00:23:37.263Z" },
- { url = "https://files.pythonhosted.org/packages/4c/f5/ed3fcddd0a5e39321e595e144615399e47e7c153a1fb8c4862aec3151ff9/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:c92010b58a51196a5f41c3795190203ac52edfd5dc3ff99149b4659eba9d2085", size = 4926748, upload-time = "2026-01-28T00:23:38.884Z" },
- { url = "https://files.pythonhosted.org/packages/43/ae/9f03d5f0c0c00e85ecb34f06d3b79599f20630e4db91b8a6e56e8f83d410/cryptography-46.0.4-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:829c2b12bbc5428ab02d6b7f7e9bbfd53e33efd6672d21341f2177470171ad8b", size = 4442307, upload-time = "2026-01-28T00:23:40.56Z" },
- { url = "https://files.pythonhosted.org/packages/8b/22/e0f9f2dae8040695103369cf2283ef9ac8abe4d51f68710bec2afd232609/cryptography-46.0.4-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:62217ba44bf81b30abaeda1488686a04a702a261e26f87db51ff61d9d3510abd", size = 3959253, upload-time = "2026-01-28T00:23:42.827Z" },
- { url = "https://files.pythonhosted.org/packages/01/5b/6a43fcccc51dae4d101ac7d378a8724d1ba3de628a24e11bf2f4f43cba4d/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:9c2da296c8d3415b93e6053f5a728649a87a48ce084a9aaf51d6e46c87c7f2d2", size = 4269372, upload-time = "2026-01-28T00:23:44.655Z" },
- { url = "https://files.pythonhosted.org/packages/17/b7/0f6b8c1dd0779df2b526e78978ff00462355e31c0a6f6cff8a3e99889c90/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9b34d8ba84454641a6bf4d6762d15847ecbd85c1316c0a7984e6e4e9f748ec2e", size = 4891908, upload-time = "2026-01-28T00:23:46.48Z" },
- { url = "https://files.pythonhosted.org/packages/83/17/259409b8349aa10535358807a472c6a695cf84f106022268d31cea2b6c97/cryptography-46.0.4-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:df4a817fa7138dd0c96c8c8c20f04b8aaa1fac3bbf610913dcad8ea82e1bfd3f", size = 4441254, upload-time = "2026-01-28T00:23:48.403Z" },
- { url = "https://files.pythonhosted.org/packages/9c/fe/e4a1b0c989b00cee5ffa0764401767e2d1cf59f45530963b894129fd5dce/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b1de0ebf7587f28f9190b9cb526e901bf448c9e6a99655d2b07fff60e8212a82", size = 4396520, upload-time = "2026-01-28T00:23:50.26Z" },
- { url = "https://files.pythonhosted.org/packages/b3/81/ba8fd9657d27076eb40d6a2f941b23429a3c3d2f56f5a921d6b936a27bc9/cryptography-46.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9b4d17bc7bd7cdd98e3af40b441feaea4c68225e2eb2341026c84511ad246c0c", size = 4651479, upload-time = "2026-01-28T00:23:51.674Z" },
- { url = "https://files.pythonhosted.org/packages/00/03/0de4ed43c71c31e4fe954edd50b9d28d658fef56555eba7641696370a8e2/cryptography-46.0.4-cp314-cp314t-win32.whl", hash = "sha256:c411f16275b0dea722d76544a61d6421e2cc829ad76eec79280dbdc9ddf50061", size = 3001986, upload-time = "2026-01-28T00:23:53.485Z" },
- { url = "https://files.pythonhosted.org/packages/5c/70/81830b59df7682917d7a10f833c4dab2a5574cd664e86d18139f2b421329/cryptography-46.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:728fedc529efc1439eb6107b677f7f7558adab4553ef8669f0d02d42d7b959a7", size = 3468288, upload-time = "2026-01-28T00:23:55.09Z" },
{ url = "https://files.pythonhosted.org/packages/56/f7/f648fdbb61d0d45902d3f374217451385edc7e7768d1b03ff1d0e5ffc17b/cryptography-46.0.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a9556ba711f7c23f77b151d5798f3ac44a13455cc68db7697a1096e6d0563cab", size = 7169583, upload-time = "2026-01-28T00:23:56.558Z" },
{ url = "https://files.pythonhosted.org/packages/d8/cc/8f3224cbb2a928de7298d6ed4790f5ebc48114e02bdc9559196bfb12435d/cryptography-46.0.4-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8bf75b0259e87fa70bddc0b8b4078b76e7fd512fd9afae6c1193bcf440a4dbef", size = 4275419, upload-time = "2026-01-28T00:23:58.364Z" },
{ url = "https://files.pythonhosted.org/packages/17/43/4a18faa7a872d00e4264855134ba82d23546c850a70ff209e04ee200e76f/cryptography-46.0.4-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3c268a3490df22270955966ba236d6bc4a8f9b6e4ffddb78aac535f1a5ea471d", size = 4419058, upload-time = "2026-01-28T00:23:59.867Z" },
@@ -629,70 +486,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" },
{ url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" },
{ url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" },
- { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" },
- { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" },
- { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" },
- { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" },
- { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" },
- { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" },
- { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" },
- { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" },
- { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" },
- { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" },
- { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" },
- { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" },
- { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" },
- { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" },
- { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" },
- { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" },
- { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" },
- { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" },
- { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" },
- { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" },
- { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" },
- { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" },
- { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" },
- { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" },
- { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" },
- { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" },
- { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" },
- { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" },
- { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" },
- { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" },
- { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" },
- { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" },
- { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" },
- { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" },
- { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" },
- { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" },
- { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" },
- { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" },
- { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" },
- { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" },
- { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" },
- { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" },
- { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" },
- { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" },
- { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" },
- { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" },
- { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" },
- { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" },
- { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" },
- { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" },
- { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" },
- { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" },
- { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" },
- { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" },
- { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" },
- { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" },
- { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" },
- { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" },
- { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" },
- { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" },
- { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" },
- { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" },
- { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" },
- { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" },
{ url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
]
@@ -717,7 +510,7 @@ requests = [
[[package]]
name = "google-genai"
-version = "1.60.0"
+version = "1.61.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -731,9 +524,9 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "websockets" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/0a/3f/a753be0dcee352b7d63bc6d1ba14a72591d63b6391dac0cdff7ac168c530/google_genai-1.60.0.tar.gz", hash = "sha256:9768061775fddfaecfefb0d6d7a6cabefb3952ebd246cd5f65247151c07d33d1", size = 487721, upload-time = "2026-01-21T22:17:30.398Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/69/38/421cd7e70952a536be87a0249409f87297d84f523754a25b08fe94b97e7f/google_genai-1.61.0.tar.gz", hash = "sha256:5773a4e8ad5b2ebcd54a633a67d8e9c4f413032fef07977ee47ffa34a6d3bbdf", size = 489672, upload-time = "2026-01-30T20:50:27.177Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/31/e5/384b1f383917b5f0ae92e28f47bc27b16e3d26cd9bacb25e9f8ecab3c8fe/google_genai-1.60.0-py3-none-any.whl", hash = "sha256:967338378ffecebec19a8ed90cf8797b26818bacbefd7846a9280beb1099f7f3", size = 719431, upload-time = "2026-01-21T22:17:28.086Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/87/78dd70cb59f7acf3350f53c5144a7aa7bc39c6f425cd7dc1224b59fcdac3/google_genai-1.61.0-py3-none-any.whl", hash = "sha256:cb073ef8287581476c1c3f4d8e735426ee34478e500a56deef218fa93071e3ca", size = 721948, upload-time = "2026-01-30T20:50:25.551Z" },
]
[[package]]
@@ -844,135 +637,6 @@ wheels = [
]
[[package]]
-name = "imbue-core"
-version = "0.0.9"
-source = { editable = "imbue_core" }
-dependencies = [
- { name = "anthropic" },
- { name = "anyio" },
- { name = "attrs" },
- { name = "boto3" },
- { name = "cachetools" },
- { name = "cattrs" },
- { name = "diskcache" },
- { name = "google-genai" },
- { name = "groq" },
- { name = "grpclib" },
- { name = "httpx" },
- { name = "inline-snapshot" },
- { name = "loguru" },
- { name = "openai" },
- { name = "pathspec" },
- { name = "prometheus-client" },
- { name = "pydantic" },
- { name = "pydantic-settings" },
- { name = "pygit2" },
- { name = "pygments" },
- { name = "pyhumps" },
- { name = "pylint" },
- { name = "pytest" },
- { name = "pytest-asyncio" },
- { name = "pytest-mock" },
- { name = "python-gitlab" },
- { name = "syrupy" },
- { name = "tblib" },
- { name = "tenacity" },
- { name = "tiktoken" },
- { name = "together" },
- { name = "toml" },
- { name = "traceback-with-variables" },
- { name = "typeid-python" },
- { name = "yasoo" },
-]
-
-[package.metadata]
-requires-dist = [
- { name = "anthropic", specifier = "~=0.54" },
- { name = "anyio" },
- { name = "attrs" },
- { name = "boto3", specifier = ">=1.38.27" },
- { name = "cachetools" },
- { name = "cattrs" },
- { name = "diskcache", specifier = ">=5.6.3" },
- { name = "google-genai", specifier = ">=1.26.0" },
- { name = "groq", specifier = ">=0.18.0" },
- { name = "grpclib", specifier = ">=0.4.7" },
- { name = "httpx" },
- { name = "inline-snapshot" },
- { name = "loguru" },
- { name = "openai", specifier = ">=1.79.0" },
- { name = "pathspec" },
- { name = "prometheus-client", specifier = ">=0.20.0" },
- { name = "pydantic", specifier = ">=2.11.4" },
- { name = "pydantic-settings" },
- { name = "pygit2", specifier = ">=1.18.0" },
- { name = "pygments", specifier = ">=2.0.0" },
- { name = "pyhumps" },
- { name = "pylint", specifier = "==3.2.6" },
- { name = "pytest" },
- { name = "pytest-asyncio" },
- { name = "pytest-mock" },
- { name = "python-gitlab", specifier = ">=4.5.0" },
- { name = "syrupy" },
- { name = "tblib", specifier = "==2.0.0" },
- { name = "tenacity", specifier = ">=8.2.2" },
- { name = "tiktoken" },
- { name = "together" },
- { name = "toml" },
- { name = "traceback-with-variables", specifier = ">=2.2.0" },
- { name = "typeid-python" },
- { name = "yasoo" },
-]
-
-[package.metadata.requires-dev]
-dev = [{ name = "moto", specifier = ">=4.1.12" }]
-
-[[package]]
-name = "imbue-tools"
-version = "0.1.0"
-source = { editable = "imbue_tools" }
-dependencies = [
- { name = "anyio" },
- { name = "async-lru" },
- { name = "attrs" },
- { name = "imbue-core" },
- { name = "jinja2" },
- { name = "libcst" },
- { name = "loguru" },
- { name = "psycopg", extra = ["binary"] },
- { name = "pydantic" },
- { name = "pydantic-settings" },
- { name = "pygit2" },
- { name = "pytest" },
- { name = "pytest-asyncio" },
- { name = "python-gitlab" },
- { name = "requests" },
- { name = "syrupy" },
-]
-
-[package.metadata]
-requires-dist = [
- { name = "anyio" },
- { name = "async-lru" },
- { name = "attrs" },
- { name = "imbue-core", editable = "imbue_core" },
- { name = "jinja2" },
- { name = "libcst" },
- { name = "loguru" },
- { name = "psycopg", extras = ["binary"] },
- { name = "pydantic" },
- { name = "pydantic-settings" },
- { name = "pygit2" },
- { name = "pytest" },
- { name = "pytest-asyncio" },
- { name = "python-gitlab" },
- { name = "requests" },
- { name = "syrupy" },
- { name = "vet", marker = "extra == 'test'", editable = "." },
-]
-provides-extras = ["test"]
-
-[[package]]
name = "iniconfig"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
@@ -1019,87 +683,44 @@ wheels = [
[[package]]
name = "jiter"
-version = "0.12.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435, upload-time = "2025-11-09T20:47:02.087Z" },
- { url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548, upload-time = "2025-11-09T20:47:03.409Z" },
- { url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915, upload-time = "2025-11-09T20:47:05.171Z" },
- { url = "https://files.pythonhosted.org/packages/15/2d/c06e659888c128ad1e838123d0638f0efad90cc30860cb5f74dd3f2fc0b3/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966, upload-time = "2025-11-09T20:47:06.508Z" },
- { url = "https://files.pythonhosted.org/packages/6b/20/058db4ae5fb07cf6a4ab2e9b9294416f606d8e467fb74c2184b2a1eeacba/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047, upload-time = "2025-11-09T20:47:08.382Z" },
- { url = "https://files.pythonhosted.org/packages/49/bb/dc2b1c122275e1de2eb12905015d61e8316b2f888bdaac34221c301495d6/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835, upload-time = "2025-11-09T20:47:09.81Z" },
- { url = "https://files.pythonhosted.org/packages/23/7d/38f9cd337575349de16da575ee57ddb2d5a64d425c9367f5ef9e4612e32e/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587, upload-time = "2025-11-09T20:47:11.529Z" },
- { url = "https://files.pythonhosted.org/packages/f0/a3/b13e8e61e70f0bb06085099c4e2462647f53cc2ca97614f7fedcaa2bb9f3/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492, upload-time = "2025-11-09T20:47:12.993Z" },
- { url = "https://files.pythonhosted.org/packages/07/71/e0d11422ed027e21422f7bc1883c61deba2d9752b720538430c1deadfbca/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046, upload-time = "2025-11-09T20:47:14.6Z" },
- { url = "https://files.pythonhosted.org/packages/9f/59/b968a9aa7102a8375dbbdfbd2aeebe563c7e5dddf0f47c9ef1588a97e224/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392, upload-time = "2025-11-09T20:47:16.011Z" },
- { url = "https://files.pythonhosted.org/packages/ca/e4/7df62002499080dbd61b505c5cb351aa09e9959d176cac2aa8da6f93b13b/jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c", size = 206096, upload-time = "2025-11-09T20:47:17.344Z" },
- { url = "https://files.pythonhosted.org/packages/bb/60/1032b30ae0572196b0de0e87dce3b6c26a1eff71aad5fe43dee3082d32e0/jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f", size = 204899, upload-time = "2025-11-09T20:47:19.365Z" },
- { url = "https://files.pythonhosted.org/packages/49/d5/c145e526fccdb834063fb45c071df78b0cc426bbaf6de38b0781f45d956f/jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5", size = 188070, upload-time = "2025-11-09T20:47:20.75Z" },
- { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" },
- { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" },
- { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" },
- { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" },
- { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" },
- { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" },
- { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" },
- { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" },
- { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" },
- { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" },
- { url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546, upload-time = "2025-11-09T20:47:40.47Z" },
- { url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196, upload-time = "2025-11-09T20:47:41.794Z" },
- { url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100, upload-time = "2025-11-09T20:47:43.007Z" },
- { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" },
- { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" },
- { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" },
- { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" },
- { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" },
- { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" },
- { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" },
- { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" },
- { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" },
- { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" },
- { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" },
- { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" },
- { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" },
- { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" },
- { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" },
- { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" },
- { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" },
- { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" },
- { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" },
- { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" },
- { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" },
- { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" },
- { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" },
- { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" },
- { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" },
- { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" },
- { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" },
- { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" },
- { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" },
- { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" },
- { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" },
- { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" },
- { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" },
- { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" },
- { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" },
- { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" },
- { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" },
- { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" },
- { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" },
- { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" },
- { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" },
- { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" },
- { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" },
- { url = "https://files.pythonhosted.org/packages/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144, upload-time = "2025-11-09T20:49:10.503Z" },
- { url = "https://files.pythonhosted.org/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877, upload-time = "2025-11-09T20:49:12.269Z" },
- { url = "https://files.pythonhosted.org/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419, upload-time = "2025-11-09T20:49:13.803Z" },
- { url = "https://files.pythonhosted.org/packages/21/01/857d4608f5edb0664aa791a3d45702e1a5bcfff9934da74035e7b9803846/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d", size = 347212, upload-time = "2025-11-09T20:49:15.643Z" },
- { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" },
- { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" },
- { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" },
- { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" },
+version = "0.13.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" },
+ { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" },
+ { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" },
+ { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" },
+ { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709, upload-time = "2026-02-02T12:35:52.467Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560, upload-time = "2026-02-02T12:35:53.925Z" },
+ { url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608, upload-time = "2026-02-02T12:35:55.304Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" },
+ { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" },
+ { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" },
+ { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" },
+ { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" },
+ { url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" },
+ { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" },
+ { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" },
+ { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" },
]
[[package]]
@@ -1116,8 +737,7 @@ name = "libcst"
version = "1.8.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "pyyaml", marker = "python_full_version != '3.13.*'" },
- { name = "pyyaml-ft", marker = "python_full_version == '3.13.*'" },
+ { name = "pyyaml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/de/cd/337df968b38d94c5aabd3e1b10630f047a2b345f6e1d4456bd9fe7417537/libcst-1.8.6.tar.gz", hash = "sha256:f729c37c9317126da9475bdd06a7208eb52fcbd180a6341648b45a56b4ba708b", size = 891354, upload-time = "2025-11-03T22:33:30.621Z" }
wheels = [
@@ -1137,38 +757,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/35/1d/317ddef3669883619ef3d3395ea583305f353ef4ad87d7a5ac1c39be38e3/libcst-1.8.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:375965f34cc6f09f5f809244d3ff9bd4f6cb6699f571121cebce53622e7e0b86", size = 2408239, upload-time = "2025-11-03T22:32:23.275Z" },
{ url = "https://files.pythonhosted.org/packages/9a/a1/f47d8cccf74e212dd6044b9d6dbc223636508da99acff1d54786653196bc/libcst-1.8.6-cp312-cp312-win_amd64.whl", hash = "sha256:da95b38693b989eaa8d32e452e8261cfa77fe5babfef1d8d2ac25af8c4aa7e6d", size = 2119660, upload-time = "2025-11-03T22:32:24.822Z" },
{ url = "https://files.pythonhosted.org/packages/19/d0/dd313bf6a7942cdf951828f07ecc1a7695263f385065edc75ef3016a3cb5/libcst-1.8.6-cp312-cp312-win_arm64.whl", hash = "sha256:bff00e1c766658adbd09a175267f8b2f7616e5ee70ce45db3d7c4ce6d9f6bec7", size = 1999824, upload-time = "2025-11-03T22:32:26.131Z" },
- { url = "https://files.pythonhosted.org/packages/90/01/723cd467ec267e712480c772aacc5aa73f82370c9665162fd12c41b0065b/libcst-1.8.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7445479ebe7d1aff0ee094ab5a1c7718e1ad78d33e3241e1a1ec65dcdbc22ffb", size = 2206386, upload-time = "2025-11-03T22:32:27.422Z" },
- { url = "https://files.pythonhosted.org/packages/17/50/b944944f910f24c094f9b083f76f61e3985af5a376f5342a21e01e2d1a81/libcst-1.8.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4fc3fef8a2c983e7abf5d633e1884c5dd6fa0dcb8f6e32035abd3d3803a3a196", size = 2083945, upload-time = "2025-11-03T22:32:28.847Z" },
- { url = "https://files.pythonhosted.org/packages/36/a1/bd1b2b2b7f153d82301cdaddba787f4a9fc781816df6bdb295ca5f88b7cf/libcst-1.8.6-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:1a3a5e4ee870907aa85a4076c914ae69066715a2741b821d9bf16f9579de1105", size = 2235818, upload-time = "2025-11-03T22:32:30.504Z" },
- { url = "https://files.pythonhosted.org/packages/b9/ab/f5433988acc3b4d188c4bb154e57837df9488cc9ab551267cdeabd3bb5e7/libcst-1.8.6-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6609291c41f7ad0bac570bfca5af8fea1f4a27987d30a1fa8b67fe5e67e6c78d", size = 2301289, upload-time = "2025-11-03T22:32:31.812Z" },
- { url = "https://files.pythonhosted.org/packages/5d/57/89f4ba7a6f1ac274eec9903a9e9174890d2198266eee8c00bc27eb45ecf7/libcst-1.8.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:25eaeae6567091443b5374b4c7d33a33636a2d58f5eda02135e96fc6c8807786", size = 2299230, upload-time = "2025-11-03T22:32:33.242Z" },
- { url = "https://files.pythonhosted.org/packages/f2/36/0aa693bc24cce163a942df49d36bf47a7ed614a0cd5598eee2623bc31913/libcst-1.8.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04030ea4d39d69a65873b1d4d877def1c3951a7ada1824242539e399b8763d30", size = 2408519, upload-time = "2025-11-03T22:32:34.678Z" },
- { url = "https://files.pythonhosted.org/packages/db/18/6dd055b5f15afa640fb3304b2ee9df8b7f72e79513814dbd0a78638f4a0e/libcst-1.8.6-cp313-cp313-win_amd64.whl", hash = "sha256:8066f1b70f21a2961e96bedf48649f27dfd5ea68be5cd1bed3742b047f14acde", size = 2119853, upload-time = "2025-11-03T22:32:36.287Z" },
- { url = "https://files.pythonhosted.org/packages/c9/ed/5ddb2a22f0b0abdd6dcffa40621ada1feaf252a15e5b2733a0a85dfd0429/libcst-1.8.6-cp313-cp313-win_arm64.whl", hash = "sha256:c188d06b583900e662cd791a3f962a8c96d3dfc9b36ea315be39e0a4c4792ebf", size = 1999808, upload-time = "2025-11-03T22:32:38.1Z" },
- { url = "https://files.pythonhosted.org/packages/25/d3/72b2de2c40b97e1ef4a1a1db4e5e52163fc7e7740ffef3846d30bc0096b5/libcst-1.8.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c41c76e034a1094afed7057023b1d8967f968782433f7299cd170eaa01ec033e", size = 2190553, upload-time = "2025-11-03T22:32:39.819Z" },
- { url = "https://files.pythonhosted.org/packages/0d/20/983b7b210ccc3ad94a82db54230e92599c4a11b9cfc7ce3bc97c1d2df75c/libcst-1.8.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5432e785322aba3170352f6e72b32bea58d28abd141ac37cc9b0bf6b7c778f58", size = 2074717, upload-time = "2025-11-03T22:32:41.373Z" },
- { url = "https://files.pythonhosted.org/packages/13/f2/9e01678fedc772e09672ed99930de7355757035780d65d59266fcee212b8/libcst-1.8.6-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:85b7025795b796dea5284d290ff69de5089fc8e989b25d6f6f15b6800be7167f", size = 2225834, upload-time = "2025-11-03T22:32:42.716Z" },
- { url = "https://files.pythonhosted.org/packages/4a/0d/7bed847b5c8c365e9f1953da274edc87577042bee5a5af21fba63276e756/libcst-1.8.6-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:536567441182a62fb706e7aa954aca034827b19746832205953b2c725d254a93", size = 2287107, upload-time = "2025-11-03T22:32:44.549Z" },
- { url = "https://files.pythonhosted.org/packages/02/f0/7e51fa84ade26c518bfbe7e2e4758b56d86a114c72d60309ac0d350426c4/libcst-1.8.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f04d3672bde1704f383a19e8f8331521abdbc1ed13abb349325a02ac56e5012", size = 2288672, upload-time = "2025-11-03T22:32:45.867Z" },
- { url = "https://files.pythonhosted.org/packages/ad/cd/15762659a3f5799d36aab1bc2b7e732672722e249d7800e3c5f943b41250/libcst-1.8.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f04febcd70e1e67917be7de513c8d4749d2e09206798558d7fe632134426ea4", size = 2392661, upload-time = "2025-11-03T22:32:47.232Z" },
- { url = "https://files.pythonhosted.org/packages/e4/6b/b7f9246c323910fcbe021241500f82e357521495dcfe419004dbb272c7cb/libcst-1.8.6-cp313-cp313t-win_amd64.whl", hash = "sha256:1dc3b897c8b0f7323412da3f4ad12b16b909150efc42238e19cbf19b561cc330", size = 2105068, upload-time = "2025-11-03T22:32:49.145Z" },
- { url = "https://files.pythonhosted.org/packages/a6/0b/4fd40607bc4807ec2b93b054594373d7fa3d31bb983789901afcb9bcebe9/libcst-1.8.6-cp313-cp313t-win_arm64.whl", hash = "sha256:44f38139fa95e488db0f8976f9c7ca39a64d6bc09f2eceef260aa1f6da6a2e42", size = 1985181, upload-time = "2025-11-03T22:32:50.597Z" },
- { url = "https://files.pythonhosted.org/packages/3a/60/4105441989e321f7ad0fd28ffccb83eb6aac0b7cfb0366dab855dcccfbe5/libcst-1.8.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b188e626ce61de5ad1f95161b8557beb39253de4ec74fc9b1f25593324a0279c", size = 2204202, upload-time = "2025-11-03T22:32:52.311Z" },
- { url = "https://files.pythonhosted.org/packages/67/2f/51a6f285c3a183e50cfe5269d4a533c21625aac2c8de5cdf2d41f079320d/libcst-1.8.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:87e74f7d7dfcba9efa91127081e22331d7c42515f0a0ac6e81d4cf2c3ed14661", size = 2083581, upload-time = "2025-11-03T22:32:54.269Z" },
- { url = "https://files.pythonhosted.org/packages/2f/64/921b1c19b638860af76cdb28bc81d430056592910b9478eea49e31a7f47a/libcst-1.8.6-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:3a926a4b42015ee24ddfc8ae940c97bd99483d286b315b3ce82f3bafd9f53474", size = 2236495, upload-time = "2025-11-03T22:32:55.723Z" },
- { url = "https://files.pythonhosted.org/packages/12/a8/b00592f9bede618cbb3df6ffe802fc65f1d1c03d48a10d353b108057d09c/libcst-1.8.6-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:3f4fbb7f569e69fd9e89d9d9caa57ca42c577c28ed05062f96a8c207594e75b8", size = 2301466, upload-time = "2025-11-03T22:32:57.337Z" },
- { url = "https://files.pythonhosted.org/packages/af/df/790d9002f31580fefd0aec2f373a0f5da99070e04c5e8b1c995d0104f303/libcst-1.8.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:08bd63a8ce674be431260649e70fca1d43f1554f1591eac657f403ff8ef82c7a", size = 2300264, upload-time = "2025-11-03T22:32:58.852Z" },
- { url = "https://files.pythonhosted.org/packages/21/de/dc3f10e65bab461be5de57850d2910a02c24c3ddb0da28f0e6e4133c3487/libcst-1.8.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e00e275d4ba95d4963431ea3e409aa407566a74ee2bf309a402f84fc744abe47", size = 2408572, upload-time = "2025-11-03T22:33:00.552Z" },
- { url = "https://files.pythonhosted.org/packages/20/3b/35645157a7590891038b077db170d6dd04335cd2e82a63bdaa78c3297dfe/libcst-1.8.6-cp314-cp314-win_amd64.whl", hash = "sha256:fea5c7fa26556eedf277d4f72779c5ede45ac3018650721edd77fd37ccd4a2d4", size = 2193917, upload-time = "2025-11-03T22:33:02.354Z" },
- { url = "https://files.pythonhosted.org/packages/b3/a2/1034a9ba7d3e82f2c2afaad84ba5180f601aed676d92b76325797ad60951/libcst-1.8.6-cp314-cp314-win_arm64.whl", hash = "sha256:bb9b4077bdf8857b2483879cbbf70f1073bc255b057ec5aac8a70d901bb838e9", size = 2078748, upload-time = "2025-11-03T22:33:03.707Z" },
- { url = "https://files.pythonhosted.org/packages/95/a1/30bc61e8719f721a5562f77695e6154e9092d1bdf467aa35d0806dcd6cea/libcst-1.8.6-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:55ec021a296960c92e5a33b8d93e8ad4182b0eab657021f45262510a58223de1", size = 2188980, upload-time = "2025-11-03T22:33:05.152Z" },
- { url = "https://files.pythonhosted.org/packages/2c/14/c660204532407c5628e3b615015a902ed2d0b884b77714a6bdbe73350910/libcst-1.8.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ba9ab2b012fbd53b36cafd8f4440a6b60e7e487cd8b87428e57336b7f38409a4", size = 2074828, upload-time = "2025-11-03T22:33:06.864Z" },
- { url = "https://files.pythonhosted.org/packages/82/e2/c497c354943dff644749f177ee9737b09ed811b8fc842b05709a40fe0d1b/libcst-1.8.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c0a0cc80aebd8aa15609dd4d330611cbc05e9b4216bcaeabba7189f99ef07c28", size = 2225568, upload-time = "2025-11-03T22:33:08.354Z" },
- { url = "https://files.pythonhosted.org/packages/86/ef/45999676d07bd6d0eefa28109b4f97124db114e92f9e108de42ba46a8028/libcst-1.8.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:42a4f68121e2e9c29f49c97f6154e8527cd31021809cc4a941c7270aa64f41aa", size = 2286523, upload-time = "2025-11-03T22:33:10.206Z" },
- { url = "https://files.pythonhosted.org/packages/f4/6c/517d8bf57d9f811862f4125358caaf8cd3320a01291b3af08f7b50719db4/libcst-1.8.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a434c521fadaf9680788b50d5c21f4048fa85ed19d7d70bd40549fbaeeecab1", size = 2288044, upload-time = "2025-11-03T22:33:11.628Z" },
- { url = "https://files.pythonhosted.org/packages/83/ce/24d7d49478ffb61207f229239879845da40a374965874f5ee60f96b02ddb/libcst-1.8.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6a65f844d813ab4ef351443badffa0ae358f98821561d19e18b3190f59e71996", size = 2392605, upload-time = "2025-11-03T22:33:12.962Z" },
- { url = "https://files.pythonhosted.org/packages/39/c3/829092ead738b71e96a4e96896c96f276976e5a8a58b4473ed813d7c962b/libcst-1.8.6-cp314-cp314t-win_amd64.whl", hash = "sha256:bdb14bc4d4d83a57062fed2c5da93ecb426ff65b0dc02ddf3481040f5f074a82", size = 2181581, upload-time = "2025-11-03T22:33:14.514Z" },
- { url = "https://files.pythonhosted.org/packages/98/6d/5d6a790a02eb0d9d36c4aed4f41b277497e6178900b2fa29c35353aa45ed/libcst-1.8.6-cp314-cp314t-win_arm64.whl", hash = "sha256:819c8081e2948635cab60c603e1bbdceccdfe19104a242530ad38a36222cb88f", size = 2065000, upload-time = "2025-11-03T22:33:16.257Z" },
]
[[package]]
@@ -1224,50 +812,6 @@ wheels = [
{ 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]]
@@ -1298,6 +842,26 @@ wheels = [
]
[[package]]
+name = "moto"
+version = "5.1.20"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "boto3" },
+ { name = "botocore" },
+ { name = "cryptography" },
+ { name = "jinja2" },
+ { name = "python-dateutil" },
+ { name = "requests" },
+ { name = "responses" },
+ { name = "werkzeug" },
+ { name = "xmltodict" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b4/93/6b696aab5174721696a17716a488086e21f7b2547b4c9517f799a9b25e9e/moto-5.1.20.tar.gz", hash = "sha256:6d12d781e26a550d80e4b7e01d5538178e3adec6efbdec870e06e84750f13ec0", size = 8318716, upload-time = "2026-01-17T21:49:00.101Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7f/2f/f50892fdb28097917b87d358a5fcefd30976289884ff142893edcb0243ba/moto-5.1.20-py3-none-any.whl", hash = "sha256:58c82c8e6b2ef659ef3a562fa415dce14da84bc7a797943245d9a338496ea0ea", size = 6392751, upload-time = "2026-01-17T21:48:57.099Z" },
+]
+
+[[package]]
name = "multidict"
version = "6.7.1"
source = { registry = "https://pypi.org/simple" }
@@ -1339,78 +903,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" },
{ url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" },
{ url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" },
- { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" },
- { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" },
- { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" },
- { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" },
- { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" },
- { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" },
- { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" },
- { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" },
- { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" },
- { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" },
- { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" },
- { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" },
- { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" },
- { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" },
- { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" },
- { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" },
- { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" },
- { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" },
- { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" },
- { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" },
- { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" },
- { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" },
- { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" },
- { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" },
- { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" },
- { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" },
- { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" },
- { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" },
- { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" },
- { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" },
- { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" },
- { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" },
- { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" },
- { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" },
- { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" },
- { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" },
- { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" },
- { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" },
- { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" },
- { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" },
- { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" },
- { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" },
- { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" },
- { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" },
- { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" },
- { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" },
- { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" },
- { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" },
- { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" },
- { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" },
- { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" },
- { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" },
- { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" },
- { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" },
- { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" },
- { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" },
- { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" },
- { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" },
- { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" },
- { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" },
- { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" },
- { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" },
- { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" },
- { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" },
- { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" },
- { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" },
- { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" },
- { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" },
- { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" },
- { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" },
- { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" },
- { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" },
{ url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" },
]
@@ -1425,81 +917,39 @@ wheels = [
[[package]]
name = "numpy"
-version = "2.4.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/24/62/ae72ff66c0f1fd959925b4c11f8c2dea61f47f6acaea75a08512cdfe3fed/numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690", size = 20721320, upload-time = "2026-01-10T06:44:59.619Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a5/34/2b1bc18424f3ad9af577f6ce23600319968a70575bd7db31ce66731bbef9/numpy-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0cce2a669e3c8ba02ee563c7835f92c153cf02edff1ae05e1823f1dde21b16a5", size = 16944563, upload-time = "2026-01-10T06:42:14.615Z" },
- { url = "https://files.pythonhosted.org/packages/2c/57/26e5f97d075aef3794045a6ca9eada6a4ed70eb9a40e7a4a93f9ac80d704/numpy-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:899d2c18024984814ac7e83f8f49d8e8180e2fbe1b2e252f2e7f1d06bea92425", size = 12645658, upload-time = "2026-01-10T06:42:17.298Z" },
- { url = "https://files.pythonhosted.org/packages/8e/ba/80fc0b1e3cb2fd5c6143f00f42eb67762aa043eaa05ca924ecc3222a7849/numpy-2.4.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:09aa8a87e45b55a1c2c205d42e2808849ece5c484b2aab11fecabec3841cafba", size = 5474132, upload-time = "2026-01-10T06:42:19.637Z" },
- { url = "https://files.pythonhosted.org/packages/40/ae/0a5b9a397f0e865ec171187c78d9b57e5588afc439a04ba9cab1ebb2c945/numpy-2.4.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:edee228f76ee2dab4579fad6f51f6a305de09d444280109e0f75df247ff21501", size = 6804159, upload-time = "2026-01-10T06:42:21.44Z" },
- { url = "https://files.pythonhosted.org/packages/86/9c/841c15e691c7085caa6fd162f063eff494099c8327aeccd509d1ab1e36ab/numpy-2.4.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a92f227dbcdc9e4c3e193add1a189a9909947d4f8504c576f4a732fd0b54240a", size = 14708058, upload-time = "2026-01-10T06:42:23.546Z" },
- { url = "https://files.pythonhosted.org/packages/5d/9d/7862db06743f489e6a502a3b93136d73aea27d97b2cf91504f70a27501d6/numpy-2.4.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:538bf4ec353709c765ff75ae616c34d3c3dca1a68312727e8f2676ea644f8509", size = 16651501, upload-time = "2026-01-10T06:42:25.909Z" },
- { url = "https://files.pythonhosted.org/packages/a6/9c/6fc34ebcbd4015c6e5f0c0ce38264010ce8a546cb6beacb457b84a75dfc8/numpy-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ac08c63cb7779b85e9d5318e6c3518b424bc1f364ac4cb2c6136f12e5ff2dccc", size = 16492627, upload-time = "2026-01-10T06:42:28.938Z" },
- { url = "https://files.pythonhosted.org/packages/aa/63/2494a8597502dacda439f61b3c0db4da59928150e62be0e99395c3ad23c5/numpy-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f9c360ecef085e5841c539a9a12b883dff005fbd7ce46722f5e9cef52634d82", size = 18585052, upload-time = "2026-01-10T06:42:31.312Z" },
- { url = "https://files.pythonhosted.org/packages/6a/93/098e1162ae7522fc9b618d6272b77404c4656c72432ecee3abc029aa3de0/numpy-2.4.1-cp311-cp311-win32.whl", hash = "sha256:0f118ce6b972080ba0758c6087c3617b5ba243d806268623dc34216d69099ba0", size = 6236575, upload-time = "2026-01-10T06:42:33.872Z" },
- { url = "https://files.pythonhosted.org/packages/8c/de/f5e79650d23d9e12f38a7bc6b03ea0835b9575494f8ec94c11c6e773b1b1/numpy-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:18e14c4d09d55eef39a6ab5b08406e84bc6869c1e34eef45564804f90b7e0574", size = 12604479, upload-time = "2026-01-10T06:42:35.778Z" },
- { url = "https://files.pythonhosted.org/packages/dd/65/e1097a7047cff12ce3369bd003811516b20ba1078dbdec135e1cd7c16c56/numpy-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:6461de5113088b399d655d45c3897fa188766415d0f568f175ab071c8873bd73", size = 10578325, upload-time = "2026-01-10T06:42:38.518Z" },
- { url = "https://files.pythonhosted.org/packages/78/7f/ec53e32bf10c813604edf07a3682616bd931d026fcde7b6d13195dfb684a/numpy-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2", size = 16656888, upload-time = "2026-01-10T06:42:40.913Z" },
- { url = "https://files.pythonhosted.org/packages/b8/e0/1f9585d7dae8f14864e948fd7fa86c6cb72dee2676ca2748e63b1c5acfe0/numpy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8", size = 12373956, upload-time = "2026-01-10T06:42:43.091Z" },
- { url = "https://files.pythonhosted.org/packages/8e/43/9762e88909ff2326f5e7536fa8cb3c49fb03a7d92705f23e6e7f553d9cb3/numpy-2.4.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a", size = 5202567, upload-time = "2026-01-10T06:42:45.107Z" },
- { url = "https://files.pythonhosted.org/packages/4b/ee/34b7930eb61e79feb4478800a4b95b46566969d837546aa7c034c742ef98/numpy-2.4.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0", size = 6549459, upload-time = "2026-01-10T06:42:48.152Z" },
- { url = "https://files.pythonhosted.org/packages/79/e3/5f115fae982565771be994867c89bcd8d7208dbfe9469185497d70de5ddf/numpy-2.4.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c", size = 14404859, upload-time = "2026-01-10T06:42:49.947Z" },
- { url = "https://files.pythonhosted.org/packages/d9/7d/9c8a781c88933725445a859cac5d01b5871588a15969ee6aeb618ba99eee/numpy-2.4.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02", size = 16371419, upload-time = "2026-01-10T06:42:52.409Z" },
- { url = "https://files.pythonhosted.org/packages/a6/d2/8aa084818554543f17cf4162c42f162acbd3bb42688aefdba6628a859f77/numpy-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162", size = 16182131, upload-time = "2026-01-10T06:42:54.694Z" },
- { url = "https://files.pythonhosted.org/packages/60/db/0425216684297c58a8df35f3284ef56ec4a043e6d283f8a59c53562caf1b/numpy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9", size = 18295342, upload-time = "2026-01-10T06:42:56.991Z" },
- { url = "https://files.pythonhosted.org/packages/31/4c/14cb9d86240bd8c386c881bafbe43f001284b7cce3bc01623ac9475da163/numpy-2.4.1-cp312-cp312-win32.whl", hash = "sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f", size = 5959015, upload-time = "2026-01-10T06:42:59.631Z" },
- { url = "https://files.pythonhosted.org/packages/51/cf/52a703dbeb0c65807540d29699fef5fda073434ff61846a564d5c296420f/numpy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87", size = 12310730, upload-time = "2026-01-10T06:43:01.627Z" },
- { url = "https://files.pythonhosted.org/packages/69/80/a828b2d0ade5e74a9fe0f4e0a17c30fdc26232ad2bc8c9f8b3197cf7cf18/numpy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8", size = 10312166, upload-time = "2026-01-10T06:43:03.673Z" },
- { url = "https://files.pythonhosted.org/packages/04/68/732d4b7811c00775f3bd522a21e8dd5a23f77eb11acdeb663e4a4ebf0ef4/numpy-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d797454e37570cfd61143b73b8debd623c3c0952959adb817dd310a483d58a1b", size = 16652495, upload-time = "2026-01-10T06:43:06.283Z" },
- { url = "https://files.pythonhosted.org/packages/20/ca/857722353421a27f1465652b2c66813eeeccea9d76d5f7b74b99f298e60e/numpy-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c55962006156aeef1629b953fd359064aa47e4d82cfc8e67f0918f7da3344f", size = 12368657, upload-time = "2026-01-10T06:43:09.094Z" },
- { url = "https://files.pythonhosted.org/packages/81/0d/2377c917513449cc6240031a79d30eb9a163d32a91e79e0da47c43f2c0c8/numpy-2.4.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:71abbea030f2cfc3092a0ff9f8c8fdefdc5e0bf7d9d9c99663538bb0ecdac0b9", size = 5197256, upload-time = "2026-01-10T06:43:13.634Z" },
- { url = "https://files.pythonhosted.org/packages/17/39/569452228de3f5de9064ac75137082c6214be1f5c532016549a7923ab4b5/numpy-2.4.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b55aa56165b17aaf15520beb9cbd33c9039810e0d9643dd4379e44294c7303e", size = 6545212, upload-time = "2026-01-10T06:43:15.661Z" },
- { url = "https://files.pythonhosted.org/packages/8c/a4/77333f4d1e4dac4395385482557aeecf4826e6ff517e32ca48e1dafbe42a/numpy-2.4.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0faba4a331195bfa96f93dd9dfaa10b2c7aa8cda3a02b7fd635e588fe821bf5", size = 14402871, upload-time = "2026-01-10T06:43:17.324Z" },
- { url = "https://files.pythonhosted.org/packages/ba/87/d341e519956273b39d8d47969dd1eaa1af740615394fe67d06f1efa68773/numpy-2.4.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e3087f53e2b4428766b54932644d148613c5a595150533ae7f00dab2f319a8", size = 16359305, upload-time = "2026-01-10T06:43:19.376Z" },
- { url = "https://files.pythonhosted.org/packages/32/91/789132c6666288eaa20ae8066bb99eba1939362e8f1a534949a215246e97/numpy-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:49e792ec351315e16da54b543db06ca8a86985ab682602d90c60ef4ff4db2a9c", size = 16181909, upload-time = "2026-01-10T06:43:21.808Z" },
- { url = "https://files.pythonhosted.org/packages/cf/b8/090b8bd27b82a844bb22ff8fdf7935cb1980b48d6e439ae116f53cdc2143/numpy-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79e9e06c4c2379db47f3f6fc7a8652e7498251789bf8ff5bd43bf478ef314ca2", size = 18284380, upload-time = "2026-01-10T06:43:23.957Z" },
- { url = "https://files.pythonhosted.org/packages/67/78/722b62bd31842ff029412271556a1a27a98f45359dea78b1548a3a9996aa/numpy-2.4.1-cp313-cp313-win32.whl", hash = "sha256:3d1a100e48cb266090a031397863ff8a30050ceefd798f686ff92c67a486753d", size = 5957089, upload-time = "2026-01-10T06:43:27.535Z" },
- { url = "https://files.pythonhosted.org/packages/da/a6/cf32198b0b6e18d4fbfa9a21a992a7fca535b9bb2b0cdd217d4a3445b5ca/numpy-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:92a0e65272fd60bfa0d9278e0484c2f52fe03b97aedc02b357f33fe752c52ffb", size = 12307230, upload-time = "2026-01-10T06:43:29.298Z" },
- { url = "https://files.pythonhosted.org/packages/44/6c/534d692bfb7d0afe30611320c5fb713659dcb5104d7cc182aff2aea092f5/numpy-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:20d4649c773f66cc2fc36f663e091f57c3b7655f936a4c681b4250855d1da8f5", size = 10313125, upload-time = "2026-01-10T06:43:31.782Z" },
- { url = "https://files.pythonhosted.org/packages/da/a1/354583ac5c4caa566de6ddfbc42744409b515039e085fab6e0ff942e0df5/numpy-2.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f93bc6892fe7b0663e5ffa83b61aab510aacffd58c16e012bb9352d489d90cb7", size = 12496156, upload-time = "2026-01-10T06:43:34.237Z" },
- { url = "https://files.pythonhosted.org/packages/51/b0/42807c6e8cce58c00127b1dc24d365305189991f2a7917aa694a109c8d7d/numpy-2.4.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:178de8f87948163d98a4c9ab5bee4ce6519ca918926ec8df195af582de28544d", size = 5324663, upload-time = "2026-01-10T06:43:36.211Z" },
- { url = "https://files.pythonhosted.org/packages/fe/55/7a621694010d92375ed82f312b2f28017694ed784775269115323e37f5e2/numpy-2.4.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:98b35775e03ab7f868908b524fc0a84d38932d8daf7b7e1c3c3a1b6c7a2c9f15", size = 6645224, upload-time = "2026-01-10T06:43:37.884Z" },
- { url = "https://files.pythonhosted.org/packages/50/96/9fa8635ed9d7c847d87e30c834f7109fac5e88549d79ef3324ab5c20919f/numpy-2.4.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941c2a93313d030f219f3a71fd3d91a728b82979a5e8034eb2e60d394a2b83f9", size = 14462352, upload-time = "2026-01-10T06:43:39.479Z" },
- { url = "https://files.pythonhosted.org/packages/03/d1/8cf62d8bb2062da4fb82dd5d49e47c923f9c0738032f054e0a75342faba7/numpy-2.4.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:529050522e983e00a6c1c6b67411083630de8b57f65e853d7b03d9281b8694d2", size = 16407279, upload-time = "2026-01-10T06:43:41.93Z" },
- { url = "https://files.pythonhosted.org/packages/86/1c/95c86e17c6b0b31ce6ef219da00f71113b220bcb14938c8d9a05cee0ff53/numpy-2.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2302dc0224c1cbc49bb94f7064f3f923a971bfae45c33870dcbff63a2a550505", size = 16248316, upload-time = "2026-01-10T06:43:44.121Z" },
- { url = "https://files.pythonhosted.org/packages/30/b4/e7f5ff8697274c9d0fa82398b6a372a27e5cef069b37df6355ccb1f1db1a/numpy-2.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9171a42fcad32dcf3fa86f0a4faa5e9f8facefdb276f54b8b390d90447cff4e2", size = 18329884, upload-time = "2026-01-10T06:43:46.613Z" },
- { url = "https://files.pythonhosted.org/packages/37/a4/b073f3e9d77f9aec8debe8ca7f9f6a09e888ad1ba7488f0c3b36a94c03ac/numpy-2.4.1-cp313-cp313t-win32.whl", hash = "sha256:382ad67d99ef49024f11d1ce5dcb5ad8432446e4246a4b014418ba3a1175a1f4", size = 6081138, upload-time = "2026-01-10T06:43:48.854Z" },
- { url = "https://files.pythonhosted.org/packages/16/16/af42337b53844e67752a092481ab869c0523bc95c4e5c98e4dac4e9581ac/numpy-2.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:62fea415f83ad8fdb6c20840578e5fbaf5ddd65e0ec6c3c47eda0f69da172510", size = 12447478, upload-time = "2026-01-10T06:43:50.476Z" },
- { url = "https://files.pythonhosted.org/packages/6c/f8/fa85b2eac68ec631d0b631abc448552cb17d39afd17ec53dcbcc3537681a/numpy-2.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a7870e8c5fc11aef57d6fea4b4085e537a3a60ad2cdd14322ed531fdca68d261", size = 10382981, upload-time = "2026-01-10T06:43:52.575Z" },
- { url = "https://files.pythonhosted.org/packages/1b/a7/ef08d25698e0e4b4efbad8d55251d20fe2a15f6d9aa7c9b30cd03c165e6f/numpy-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3869ea1ee1a1edc16c29bbe3a2f2a4e515cc3a44d43903ad41e0cacdbaf733dc", size = 16652046, upload-time = "2026-01-10T06:43:54.797Z" },
- { url = "https://files.pythonhosted.org/packages/8f/39/e378b3e3ca13477e5ac70293ec027c438d1927f18637e396fe90b1addd72/numpy-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e867df947d427cdd7a60e3e271729090b0f0df80f5f10ab7dd436f40811699c3", size = 12378858, upload-time = "2026-01-10T06:43:57.099Z" },
- { url = "https://files.pythonhosted.org/packages/c3/74/7ec6154f0006910ed1fdbb7591cf4432307033102b8a22041599935f8969/numpy-2.4.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e3bd2cb07841166420d2fa7146c96ce00cb3410664cbc1a6be028e456c4ee220", size = 5207417, upload-time = "2026-01-10T06:43:59.037Z" },
- { url = "https://files.pythonhosted.org/packages/f7/b7/053ac11820d84e42f8feea5cb81cc4fcd1091499b45b1ed8c7415b1bf831/numpy-2.4.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:f0a90aba7d521e6954670550e561a4cb925713bd944445dbe9e729b71f6cabee", size = 6542643, upload-time = "2026-01-10T06:44:01.852Z" },
- { url = "https://files.pythonhosted.org/packages/c0/c4/2e7908915c0e32ca636b92e4e4a3bdec4cb1e7eb0f8aedf1ed3c68a0d8cd/numpy-2.4.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d558123217a83b2d1ba316b986e9248a1ed1971ad495963d555ccd75dcb1556", size = 14418963, upload-time = "2026-01-10T06:44:04.047Z" },
- { url = "https://files.pythonhosted.org/packages/eb/c0/3ed5083d94e7ffd7c404e54619c088e11f2e1939a9544f5397f4adb1b8ba/numpy-2.4.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f44de05659b67d20499cbc96d49f2650769afcb398b79b324bb6e297bfe3844", size = 16363811, upload-time = "2026-01-10T06:44:06.207Z" },
- { url = "https://files.pythonhosted.org/packages/0e/68/42b66f1852bf525050a67315a4fb94586ab7e9eaa541b1bef530fab0c5dd/numpy-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:69e7419c9012c4aaf695109564e3387f1259f001b4326dfa55907b098af082d3", size = 16197643, upload-time = "2026-01-10T06:44:08.33Z" },
- { url = "https://files.pythonhosted.org/packages/d2/40/e8714fc933d85f82c6bfc7b998a0649ad9769a32f3494ba86598aaf18a48/numpy-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffd257026eb1b34352e749d7cc1678b5eeec3e329ad8c9965a797e08ccba205", size = 18289601, upload-time = "2026-01-10T06:44:10.841Z" },
- { url = "https://files.pythonhosted.org/packages/80/9a/0d44b468cad50315127e884802351723daca7cf1c98d102929468c81d439/numpy-2.4.1-cp314-cp314-win32.whl", hash = "sha256:727c6c3275ddefa0dc078524a85e064c057b4f4e71ca5ca29a19163c607be745", size = 6005722, upload-time = "2026-01-10T06:44:13.332Z" },
- { url = "https://files.pythonhosted.org/packages/7e/bb/c6513edcce5a831810e2dddc0d3452ce84d208af92405a0c2e58fd8e7881/numpy-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:7d5d7999df434a038d75a748275cd6c0094b0ecdb0837342b332a82defc4dc4d", size = 12438590, upload-time = "2026-01-10T06:44:15.006Z" },
- { url = "https://files.pythonhosted.org/packages/e9/da/a598d5cb260780cf4d255102deba35c1d072dc028c4547832f45dd3323a8/numpy-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:ce9ce141a505053b3c7bce3216071f3bf5c182b8b28930f14cd24d43932cd2df", size = 10596180, upload-time = "2026-01-10T06:44:17.386Z" },
- { url = "https://files.pythonhosted.org/packages/de/bc/ea3f2c96fcb382311827231f911723aeff596364eb6e1b6d1d91128aa29b/numpy-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e53170557d37ae404bf8d542ca5b7c629d6efa1117dac6a83e394142ea0a43f", size = 12498774, upload-time = "2026-01-10T06:44:19.467Z" },
- { url = "https://files.pythonhosted.org/packages/aa/ab/ef9d939fe4a812648c7a712610b2ca6140b0853c5efea361301006c02ae5/numpy-2.4.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:a73044b752f5d34d4232f25f18160a1cc418ea4507f5f11e299d8ac36875f8a0", size = 5327274, upload-time = "2026-01-10T06:44:23.189Z" },
- { url = "https://files.pythonhosted.org/packages/bd/31/d381368e2a95c3b08b8cf7faac6004849e960f4a042d920337f71cef0cae/numpy-2.4.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:fb1461c99de4d040666ca0444057b06541e5642f800b71c56e6ea92d6a853a0c", size = 6648306, upload-time = "2026-01-10T06:44:25.012Z" },
- { url = "https://files.pythonhosted.org/packages/c8/e5/0989b44ade47430be6323d05c23207636d67d7362a1796ccbccac6773dd2/numpy-2.4.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423797bdab2eeefbe608d7c1ec7b2b4fd3c58d51460f1ee26c7500a1d9c9ee93", size = 14464653, upload-time = "2026-01-10T06:44:26.706Z" },
- { url = "https://files.pythonhosted.org/packages/10/a7/cfbe475c35371cae1358e61f20c5f075badc18c4797ab4354140e1d283cf/numpy-2.4.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52b5f61bdb323b566b528899cc7db2ba5d1015bda7ea811a8bcf3c89c331fa42", size = 16405144, upload-time = "2026-01-10T06:44:29.378Z" },
- { url = "https://files.pythonhosted.org/packages/f8/a3/0c63fe66b534888fa5177cc7cef061541064dbe2b4b60dcc60ffaf0d2157/numpy-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42d7dd5fa36d16d52a84f821eb96031836fd405ee6955dd732f2023724d0aa01", size = 16247425, upload-time = "2026-01-10T06:44:31.721Z" },
- { url = "https://files.pythonhosted.org/packages/6b/2b/55d980cfa2c93bd40ff4c290bf824d792bd41d2fe3487b07707559071760/numpy-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7b6b5e28bbd47b7532698e5db2fe1db693d84b58c254e4389d99a27bb9b8f6b", size = 18330053, upload-time = "2026-01-10T06:44:34.617Z" },
- { url = "https://files.pythonhosted.org/packages/23/12/8b5fc6b9c487a09a7957188e0943c9ff08432c65e34567cabc1623b03a51/numpy-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:5de60946f14ebe15e713a6f22850c2372fa72f4ff9a432ab44aa90edcadaa65a", size = 6152482, upload-time = "2026-01-10T06:44:36.798Z" },
- { url = "https://files.pythonhosted.org/packages/00/a5/9f8ca5856b8940492fc24fbe13c1bc34d65ddf4079097cf9e53164d094e1/numpy-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f085da926c0d491ffff3096f91078cc97ea67e7e6b65e490bc8dcda65663be2", size = 12627117, upload-time = "2026-01-10T06:44:38.828Z" },
- { url = "https://files.pythonhosted.org/packages/ad/0d/eca3d962f9eef265f01a8e0d20085c6dd1f443cbffc11b6dede81fd82356/numpy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295", size = 10667121, upload-time = "2026-01-10T06:44:41.644Z" },
- { url = "https://files.pythonhosted.org/packages/1e/48/d86f97919e79314a1cdee4c832178763e6e98e623e123d0bada19e92c15a/numpy-2.4.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8ad35f20be147a204e28b6a0575fbf3540c5e5f802634d4258d55b1ff5facce1", size = 16822202, upload-time = "2026-01-10T06:44:43.738Z" },
- { url = "https://files.pythonhosted.org/packages/51/e9/1e62a7f77e0f37dcfb0ad6a9744e65df00242b6ea37dfafb55debcbf5b55/numpy-2.4.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:8097529164c0f3e32bb89412a0905d9100bf434d9692d9fc275e18dcf53c9344", size = 12569985, upload-time = "2026-01-10T06:44:45.945Z" },
- { url = "https://files.pythonhosted.org/packages/c7/7e/914d54f0c801342306fdcdce3e994a56476f1b818c46c47fc21ae968088c/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:ea66d2b41ca4a1630aae5507ee0a71647d3124d1741980138aa8f28f44dac36e", size = 5398484, upload-time = "2026-01-10T06:44:48.012Z" },
- { url = "https://files.pythonhosted.org/packages/1c/d8/9570b68584e293a33474e7b5a77ca404f1dcc655e40050a600dee81d27fb/numpy-2.4.1-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:d3f8f0df9f4b8be57b3bf74a1d087fec68f927a2fab68231fdb442bf2c12e426", size = 6713216, upload-time = "2026-01-10T06:44:49.725Z" },
- { url = "https://files.pythonhosted.org/packages/33/9b/9dd6e2db8d49eb24f86acaaa5258e5f4c8ed38209a4ee9de2d1a0ca25045/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2023ef86243690c2791fd6353e5b4848eedaa88ca8a2d129f462049f6d484696", size = 14538937, upload-time = "2026-01-10T06:44:51.498Z" },
- { url = "https://files.pythonhosted.org/packages/53/87/d5bd995b0f798a37105b876350d346eea5838bd8f77ea3d7a48392f3812b/numpy-2.4.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8361ea4220d763e54cff2fbe7d8c93526b744f7cd9ddab47afeff7e14e8503be", size = 16479830, upload-time = "2026-01-10T06:44:53.931Z" },
- { url = "https://files.pythonhosted.org/packages/5b/c7/b801bf98514b6ae6475e941ac05c58e6411dd863ea92916bfd6d510b08c1/numpy-2.4.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4f1b68ff47680c2925f8063402a693ede215f0257f02596b1318ecdfb1d79e33", size = 12492579, upload-time = "2026-01-10T06:44:57.094Z" },
+version = "2.4.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651, upload-time = "2026-01-31T23:13:10.135Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478, upload-time = "2026-01-31T23:10:25.623Z" },
+ { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467, upload-time = "2026-01-31T23:10:28.186Z" },
+ { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172, upload-time = "2026-01-31T23:10:30.848Z" },
+ { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145, upload-time = "2026-01-31T23:10:32.352Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084, upload-time = "2026-01-31T23:10:34.502Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477, upload-time = "2026-01-31T23:10:37.075Z" },
+ { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429, upload-time = "2026-01-31T23:10:39.704Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109, upload-time = "2026-01-31T23:10:41.924Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915, upload-time = "2026-01-31T23:10:45.26Z" },
+ { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972, upload-time = "2026-01-31T23:10:47.021Z" },
+ { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763, upload-time = "2026-01-31T23:10:50.087Z" },
+ { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963, upload-time = "2026-01-31T23:10:52.147Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571, upload-time = "2026-01-31T23:10:54.789Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469, upload-time = "2026-01-31T23:10:57.343Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820, upload-time = "2026-01-31T23:10:59.429Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067, upload-time = "2026-01-31T23:11:01.291Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782, upload-time = "2026-01-31T23:11:03.669Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128, upload-time = "2026-01-31T23:11:05.913Z" },
+ { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324, upload-time = "2026-01-31T23:11:08.248Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282, upload-time = "2026-01-31T23:11:10.497Z" },
+ { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210, upload-time = "2026-01-31T23:11:12.176Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171, upload-time = "2026-01-31T23:11:14.684Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179, upload-time = "2026-01-31T23:12:53.5Z" },
+ { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755, upload-time = "2026-01-31T23:12:55.933Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500, upload-time = "2026-01-31T23:12:58.671Z" },
+ { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252, upload-time = "2026-01-31T23:13:00.518Z" },
+ { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142, upload-time = "2026-01-31T23:13:02.219Z" },
+ { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979, upload-time = "2026-01-31T23:13:04.62Z" },
+ { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577, upload-time = "2026-01-31T23:13:07.08Z" },
]
[[package]]
@@ -1567,53 +1017,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" },
{ url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" },
{ url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" },
- { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" },
- { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" },
- { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" },
- { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" },
- { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" },
- { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" },
- { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" },
- { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" },
- { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" },
- { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" },
- { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" },
- { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" },
- { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" },
- { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" },
- { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" },
- { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" },
- { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" },
- { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" },
- { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" },
- { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" },
- { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" },
- { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" },
- { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" },
- { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" },
- { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" },
- { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" },
- { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" },
- { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" },
- { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" },
- { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" },
- { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" },
- { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" },
- { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" },
- { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" },
- { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" },
- { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" },
- { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" },
- { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" },
- { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" },
- { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" },
- { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" },
- { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" },
- { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" },
- { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" },
- { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" },
- { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" },
- { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" },
{ url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" },
{ url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" },
{ url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" },
@@ -1686,66 +1089,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" },
{ url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" },
{ url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" },
- { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" },
- { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" },
- { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" },
- { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" },
- { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" },
- { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" },
- { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" },
- { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" },
- { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" },
- { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" },
- { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" },
- { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" },
- { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" },
- { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" },
- { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" },
- { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" },
- { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" },
- { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" },
- { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" },
- { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" },
- { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" },
- { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" },
- { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" },
- { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" },
- { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" },
- { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" },
- { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" },
- { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" },
- { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" },
- { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" },
- { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" },
- { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" },
- { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" },
- { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" },
- { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" },
- { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" },
- { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" },
- { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" },
- { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" },
- { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" },
- { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" },
- { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" },
- { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" },
- { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" },
- { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" },
- { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" },
- { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" },
- { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" },
- { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" },
- { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" },
- { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" },
- { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" },
- { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" },
- { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" },
- { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" },
- { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" },
- { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" },
- { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" },
- { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" },
- { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" },
{ url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" },
]
@@ -1754,7 +1097,7 @@ name = "psycopg"
version = "3.3.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+ { name = "typing-extensions" },
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e0/1a/7d9ef4fdc13ef7f15b934c393edc97a35c281bb7d3c3329fbfcbe915a7c2/psycopg-3.3.2.tar.gz", hash = "sha256:707a67975ee214d200511177a6a80e56e654754c9afca06a7194ea6bbfde9ca7", size = 165630, upload-time = "2025-12-06T17:34:53.899Z" }
@@ -1794,28 +1137,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c6/81/cf43fb76993190cee9af1cbcfe28afb47b1928bdf45a252001017e5af26e/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8309ee4569dced5e81df5aa2dcd48c7340c8dee603a66430f042dfbd2878edca", size = 3909241, upload-time = "2025-12-06T17:33:30.092Z" },
{ url = "https://files.pythonhosted.org/packages/9d/20/c6377a0d17434674351627489deca493ea0b137c522b99c81d3a106372c8/psycopg_binary-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6464150e25b68ae3cb04c4e57496ea11ebfaae4d98126aea2f4702dd43e3c12", size = 4219746, upload-time = "2025-12-06T17:33:33.097Z" },
{ url = "https://files.pythonhosted.org/packages/25/32/716c57b28eefe02a57a4c9d5bf956849597f5ea476c7010397199e56cfde/psycopg_binary-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:716a586f99bbe4f710dc58b40069fcb33c7627e95cc6fc936f73c9235e07f9cf", size = 3537494, upload-time = "2025-12-06T17:33:35.82Z" },
- { url = "https://files.pythonhosted.org/packages/14/73/7ca7cb22b9ac7393fb5de7d28ca97e8347c375c8498b3bff2c99c1f38038/psycopg_binary-3.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc5a189e89cbfff174588665bb18d28d2d0428366cc9dae5864afcaa2e57380b", size = 4579068, upload-time = "2025-12-06T17:33:39.303Z" },
- { url = "https://files.pythonhosted.org/packages/f5/42/0cf38ff6c62c792fc5b55398a853a77663210ebd51ed6f0c4a05b06f95a6/psycopg_binary-3.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:083c2e182be433f290dc2c516fd72b9b47054fcd305cce791e0a50d9e93e06f2", size = 4657520, upload-time = "2025-12-06T17:33:42.536Z" },
- { url = "https://files.pythonhosted.org/packages/3b/60/df846bc84cbf2231e01b0fff48b09841fe486fa177665e50f4995b1bfa44/psycopg_binary-3.3.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:ac230e3643d1c436a2dfb59ca84357dfc6862c9f372fc5dbd96bafecae581f9f", size = 5452086, upload-time = "2025-12-06T17:33:46.54Z" },
- { url = "https://files.pythonhosted.org/packages/ab/85/30c846a00db86b1b53fd5bfd4b4edfbd0c00de8f2c75dd105610bd7568fc/psycopg_binary-3.3.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d8c899a540f6c7585cee53cddc929dd4d2db90fd828e37f5d4017b63acbc1a5d", size = 5131125, upload-time = "2025-12-06T17:33:50.413Z" },
- { url = "https://files.pythonhosted.org/packages/6d/15/9968732013373f36f8a2a3fb76104dffc8efd9db78709caa5ae1a87b1f80/psycopg_binary-3.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50ff10ab8c0abdb5a5451b9315538865b50ba64c907742a1385fdf5f5772b73e", size = 6722914, upload-time = "2025-12-06T17:33:54.544Z" },
- { url = "https://files.pythonhosted.org/packages/b2/ba/29e361fe02143ac5ff5a1ca3e45697344cfbebe2eaf8c4e7eec164bff9a0/psycopg_binary-3.3.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:23d2594af848c1fd3d874a9364bef50730124e72df7bb145a20cb45e728c50ed", size = 4966081, upload-time = "2025-12-06T17:33:58.477Z" },
- { url = "https://files.pythonhosted.org/packages/99/45/1be90c8f1a1a237046903e91202fb06708745c179f220b361d6333ed7641/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ea4fe6b4ead3bbbe27244ea224fcd1f53cb119afc38b71a2f3ce570149a03e30", size = 4493332, upload-time = "2025-12-06T17:34:02.011Z" },
- { url = "https://files.pythonhosted.org/packages/2e/b5/bbdc07d5f0a5e90c617abd624368182aa131485e18038b2c6c85fc054aed/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:742ce48cde825b8e52fb1a658253d6d1ff66d152081cbc76aa45e2986534858d", size = 4170781, upload-time = "2025-12-06T17:34:05.298Z" },
- { url = "https://files.pythonhosted.org/packages/d1/2a/0d45e4f4da2bd78c3237ffa03475ef3751f69a81919c54a6e610eb1a7c96/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e22bf6b54df994aff37ab52695d635f1ef73155e781eee1f5fa75bc08b58c8da", size = 3910544, upload-time = "2025-12-06T17:34:08.251Z" },
- { url = "https://files.pythonhosted.org/packages/3a/62/a8e0f092f4dbef9a94b032fb71e214cf0a375010692fbe7493a766339e47/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8db9034cde3bcdafc66980f0130813f5c5d19e74b3f2a19fb3cfbc25ad113121", size = 4220070, upload-time = "2025-12-06T17:34:11.392Z" },
- { url = "https://files.pythonhosted.org/packages/09/e6/5fc8d8aff8afa114bb4a94a0341b9309311e8bf3ab32d816032f8b984d4e/psycopg_binary-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:df65174c7cf6b05ea273ce955927d3270b3a6e27b0b12762b009ce6082b8d3fc", size = 3540922, upload-time = "2025-12-06T17:34:14.88Z" },
- { url = "https://files.pythonhosted.org/packages/bd/75/ad18c0b97b852aba286d06befb398cc6d383e9dfd0a518369af275a5a526/psycopg_binary-3.3.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9ca24062cd9b2270e4d77576042e9cc2b1d543f09da5aba1f1a3d016cea28390", size = 4596371, upload-time = "2025-12-06T17:34:18.007Z" },
- { url = "https://files.pythonhosted.org/packages/5a/79/91649d94c8d89f84af5da7c9d474bfba35b08eb8f492ca3422b08f0a6427/psycopg_binary-3.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c749770da0947bc972e512f35366dd4950c0e34afad89e60b9787a37e97cb443", size = 4675139, upload-time = "2025-12-06T17:34:21.374Z" },
- { url = "https://files.pythonhosted.org/packages/56/ac/b26e004880f054549ec9396594e1ffe435810b0673e428e619ed722e4244/psycopg_binary-3.3.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03b7cd73fb8c45d272a34ae7249713e32492891492681e3cf11dff9531cf37e9", size = 5456120, upload-time = "2025-12-06T17:34:25.102Z" },
- { url = "https://files.pythonhosted.org/packages/4b/8d/410681dccd6f2999fb115cc248521ec50dd2b0aba66ae8de7e81efdebbee/psycopg_binary-3.3.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:43b130e3b6edcb5ee856c7167ccb8561b473308c870ed83978ae478613764f1c", size = 5133484, upload-time = "2025-12-06T17:34:28.933Z" },
- { url = "https://files.pythonhosted.org/packages/66/30/ebbab99ea2cfa099d7b11b742ce13415d44f800555bfa4ad2911dc645b71/psycopg_binary-3.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1feba5a8c617922321aef945865334e468337b8fc5c73074f5e63143013b5a", size = 6731818, upload-time = "2025-12-06T17:34:33.094Z" },
- { url = "https://files.pythonhosted.org/packages/70/02/d260646253b7ad805d60e0de47f9b811d6544078452579466a098598b6f4/psycopg_binary-3.3.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cabb2a554d9a0a6bf84037d86ca91782f087dfff2a61298d0b00c19c0bc43f6d", size = 4983859, upload-time = "2025-12-06T17:34:36.457Z" },
- { url = "https://files.pythonhosted.org/packages/72/8d/e778d7bad1a7910aa36281f092bd85c5702f508fd9bb0ea2020ffbb6585c/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74bc306c4b4df35b09bc8cecf806b271e1c5d708f7900145e4e54a2e5dedfed0", size = 4516388, upload-time = "2025-12-06T17:34:40.129Z" },
- { url = "https://files.pythonhosted.org/packages/bd/f1/64e82098722e2ab3521797584caf515284be09c1e08a872551b6edbb0074/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d79b0093f0fbf7a962d6a46ae292dc056c65d16a8ee9361f3cfbafd4c197ab14", size = 4192382, upload-time = "2025-12-06T17:34:43.279Z" },
- { url = "https://files.pythonhosted.org/packages/fa/d0/c20f4e668e89494972e551c31be2a0016e3f50d552d7ae9ac07086407599/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1586e220be05547c77afc326741dd41cc7fba38a81f9931f616ae98865439678", size = 3928660, upload-time = "2025-12-06T17:34:46.757Z" },
- { url = "https://files.pythonhosted.org/packages/0f/e1/99746c171de22539fd5eb1c9ca21dc805b54cfae502d7451d237d1dbc349/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:458696a5fa5dad5b6fb5d5862c22454434ce4fe1cf66ca6c0de5f904cbc1ae3e", size = 4239169, upload-time = "2025-12-06T17:34:49.751Z" },
- { url = "https://files.pythonhosted.org/packages/72/f7/212343c1c9cfac35fd943c527af85e9091d633176e2a407a0797856ff7b9/psycopg_binary-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:04bb2de4ba69d6f8395b446ede795e8884c040ec71d01dd07ac2b2d18d4153d1", size = 3642122, upload-time = "2025-12-06T17:34:52.506Z" },
]
[[package]]
@@ -1900,48 +1221,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
{ url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
{ url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
- { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
- { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
- { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
- { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
- { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
- { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
- { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
- { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
- { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
- { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
- { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
- { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
- { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
- { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
- { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
- { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
- { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
- { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
- { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
- { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
- { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
- { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
- { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
- { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
- { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
- { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
- { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
- { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
- { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
- { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
- { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
- { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
- { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
- { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
- { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
- { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
- { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
- { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
- { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
- { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
- { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
- { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
{ url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
{ url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
{ url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
@@ -2003,36 +1282,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/45/f6a24326fb94e56ddae9906e21d4e4a006a36131a3a73819be1177e30e93/pygit2-1.19.1-cp312-cp312-win32.whl", hash = "sha256:ffe94118d39f6969fda594224b2b6df1ae79306adaf090ede65bcaf1a41b3a81", size = 942948, upload-time = "2025-12-29T11:46:54.465Z" },
{ url = "https://files.pythonhosted.org/packages/a3/1a/912ee3a33ba665f82cf8ed0087e7446f1f8e117aba1627e0c4ccc9b2a8c5/pygit2-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:c2ee3f2e91b0a5674ab7cb373234c23cf5f1cf6d84e56e6d12ff3db21414cf47", size = 1159880, upload-time = "2025-12-29T11:46:55.523Z" },
{ url = "https://files.pythonhosted.org/packages/24/fc/784eeceab43c2b4978aa46f03c267409f2502331fa18d0a8e58116d143d0/pygit2-1.19.1-cp312-cp312-win_arm64.whl", hash = "sha256:c8747d968d8d6b9d390263907f014d38a0f67bd26d8243e5bc3384cb252ec3d3", size = 966904, upload-time = "2025-12-29T11:46:56.888Z" },
- { url = "https://files.pythonhosted.org/packages/a0/2b/b3c8661e710ec49f7f38f992b913d6fef21e21ef6b9b327111b85bf1460c/pygit2-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:39af62f3e18dfdfb15c347c12b51231fdb3db3c9d5105d9046847ead14b42fce", size = 5700202, upload-time = "2025-12-29T11:46:58.294Z" },
- { url = "https://files.pythonhosted.org/packages/e2/1f/f67ec7f78a34ed14dbd3acf05ed23c4c8c2336ba6f3ca78d6b9962878435/pygit2-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed39106f1d9560709191093ed5251471dfb6b9e4aa35299dde45f4b91f7c984e", size = 5692171, upload-time = "2025-12-29T11:46:59.535Z" },
- { url = "https://files.pythonhosted.org/packages/a7/02/02f0f56b9b0b044018d9047adf68ba842ebda662ba43ace942ed904f8e9d/pygit2-1.19.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb4da746c92e23281890e865887d83f24e662fc3e1c481420e4993c5a13203fe", size = 6023018, upload-time = "2025-12-29T11:47:00.984Z" },
- { url = "https://files.pythonhosted.org/packages/da/a6/5ec78c14ca00fbffe6aa32eb6f5fbeb7fb06eb39e6929b06f7635f501a45/pygit2-1.19.1-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:93ccfab2340d38374f91ecf6cae6658bebc73883c376eb81eeb293781f6aef94", size = 4623392, upload-time = "2025-12-29T11:47:02.598Z" },
- { url = "https://files.pythonhosted.org/packages/d5/80/1a87f6e043e04cfa125380a73ef9f87a8c58292b7d4a6ed2e6203b4cd534/pygit2-1.19.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef18f1208422d3cac1c109417a5fc6143704cfff8e5de4e1665fa4a89ffe3902", size = 5786360, upload-time = "2025-12-29T11:47:03.898Z" },
- { url = "https://files.pythonhosted.org/packages/f7/6e/f5e38a4645d7fbba40083f94278814b9863b0afd14e905ebbd7ef31a27ec/pygit2-1.19.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:344f4c1e84eaa2434fbb43d96a1dd79796ab9559587a8533331fef92eab0ec7d", size = 6029576, upload-time = "2025-12-29T11:47:05.109Z" },
- { url = "https://files.pythonhosted.org/packages/53/cc/e5ff546f003c3fa635495105e3e039de3a863da66c82289b7a8baf6d5b48/pygit2-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1ae2f408206c67d395e8dc77425f8ab457cad59faaa58c700164398a62823e82", size = 5756457, upload-time = "2025-12-29T11:47:06.483Z" },
- { url = "https://files.pythonhosted.org/packages/da/dd/1331e3bdabd811992f511ebfa96f56c7b13d5f16837d74ac34dac93ce999/pygit2-1.19.1-cp313-cp313-win32.whl", hash = "sha256:9d6cf97c2da5c589b65371a8115be920cf417c46a80a2b12edb26e54a5238190", size = 942919, upload-time = "2025-12-29T11:47:07.833Z" },
- { url = "https://files.pythonhosted.org/packages/6d/01/98f74ecbe92f042d27e4de3cd7f093422d523cc67fdc74e6a65dbe4efbb8/pygit2-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:6d73aedffad280f6b655394e303533fcff15545d4d8f322011179c9474bb1b13", size = 1159846, upload-time = "2025-12-29T11:47:09.228Z" },
- { url = "https://files.pythonhosted.org/packages/27/4e/df8fa9a9f4e4e9aec417f8a674466d613985efb67453aa206f0455003738/pygit2-1.19.1-cp313-cp313-win_arm64.whl", hash = "sha256:8b067241c03a29440507e78637e233998fe1a11d2082169bd8177694ec4ee747", size = 966896, upload-time = "2025-12-29T11:47:10.233Z" },
- { url = "https://files.pythonhosted.org/packages/dd/45/1284c7714070b51e3413e66b677fa4ecf8c840d2f86d1bebc77d2390fe3d/pygit2-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d10a46285b9ae39b9de2d9f44ac7f933993aecfab189c2932320b3df596311c8", size = 5702338, upload-time = "2025-12-29T11:47:12.807Z" },
- { url = "https://files.pythonhosted.org/packages/a3/9f/7a39d4c612e12966130504e1610f500b397d7968feb6d25e1353614dab74/pygit2-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d0f3924d8d0d54a7fe186761c76dc1b6e5fcf41794a6daba1630db3bc216b9ba", size = 5692261, upload-time = "2025-12-29T11:47:14.276Z" },
- { url = "https://files.pythonhosted.org/packages/0b/7c/7806cf0ae9200bd773628be6d8c345d277b8f0161de950b572a4ce200105/pygit2-1.19.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4fcc301cfe9c29f3e29f0f80d81ae65c0bee368672b23566467dc91b5edae4b", size = 6025106, upload-time = "2025-12-29T11:47:15.904Z" },
- { url = "https://files.pythonhosted.org/packages/dc/30/7f1b67711705eb0220dcc4581a97b4aebad4ffde2f6f6b94314690e1cfa1/pygit2-1.19.1-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3c6eacf82f15e001121dc0f60057f462627045447d8bd8587b33b13159ae5155", size = 4627355, upload-time = "2025-12-29T11:47:17.365Z" },
- { url = "https://files.pythonhosted.org/packages/14/88/25f1e65ff6ed678e1be9aaeabeedcb26531d17b6b86c4b1d50d8f0c50825/pygit2-1.19.1-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:074b0b14c6f3c7e2c6ea0b01a90832407a71520c920918aa07f509c91f1691f9", size = 5788548, upload-time = "2025-12-29T11:47:18.98Z" },
- { url = "https://files.pythonhosted.org/packages/e7/5d/ff1b12d3682918ac6c3c6629a6c6272db1b4041994d38045d3c334034570/pygit2-1.19.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ada5d3e813e21918e004a33c66aba4a2b829cd5c0c0e85b92dd70f84cf95ac56", size = 6030078, upload-time = "2025-12-29T11:47:20.324Z" },
- { url = "https://files.pythonhosted.org/packages/1c/c5/c078ed6f1f5d7f3feba4b86d53e464c8358112ec32943e11e36557009818/pygit2-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:19ebe25fd8e95ed8a0be0a9dd4cecc1233db4f2a44a2a73984620909e98e907f", size = 5757154, upload-time = "2025-12-29T11:47:21.971Z" },
- { url = "https://files.pythonhosted.org/packages/76/90/1722d7c2db5d563becb59a54b2f49b44964ff699826629f96594064d972a/pygit2-1.19.1-cp314-cp314-win32.whl", hash = "sha256:5bc0738a49cceb76f0fba7cdb24532857a980e4a36b9a0da025c359dfe3676b4", size = 964159, upload-time = "2025-12-29T11:47:23.508Z" },
- { url = "https://files.pythonhosted.org/packages/74/72/80558b71ed780a732c9ff10003c3a73b68fbf320c3125ae11bb664a8076c/pygit2-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:527d40925bb85b86da0e96ecc90e9ca74d0a0273ab645bac0787b95923d93160", size = 1190612, upload-time = "2025-12-29T11:47:24.889Z" },
- { url = "https://files.pythonhosted.org/packages/28/68/c60ff9ae38543a520ca93c0d52a52c2e375ac44b9a5c5da99044cca8c5c5/pygit2-1.19.1-cp314-cp314-win_arm64.whl", hash = "sha256:21c7c8b5aa2f48cefdb8521185f0cd3c110a340e2d9f62a46a94db01a907db73", size = 994766, upload-time = "2025-12-29T11:47:25.902Z" },
- { url = "https://files.pythonhosted.org/packages/ee/42/4da546bf55183877e7da4327594ab138db92aa00921d46d513626bcad19f/pygit2-1.19.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9c5e4eb975b664b6821fe6a05b03bbc51052d1fb22f20652e1d4349ae24ed7ac", size = 5705642, upload-time = "2025-12-29T11:47:27.034Z" },
- { url = "https://files.pythonhosted.org/packages/0a/b7/74a9cf3d2e6cd6bd2fa6a7bc3530054c2f720fc59e3b731251bbdebd8983/pygit2-1.19.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8752eae5780ee51edae326cac394868917704624b63d03a5217c5e94a532a0e3", size = 5695192, upload-time = "2025-12-29T11:47:28.98Z" },
- { url = "https://files.pythonhosted.org/packages/b6/b9/bde02249c2c5deecc8e483ee9132f86f67114eec154ee10219d23a1ce9f9/pygit2-1.19.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:457f5a2e6d8527b5ad7a8bd16586c72ad2ce0aa218a37380f16d07520569ceaf", size = 6085318, upload-time = "2025-12-29T11:47:30.268Z" },
- { url = "https://files.pythonhosted.org/packages/d2/ae/b3a14edaa579700aee33a25a788f5f4fe67713a6e2273a897635e6742b35/pygit2-1.19.1-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3c8a9d53c84724c97d7e298f6628655c19f9911a90b88c362cb7d5daa645464f", size = 4684691, upload-time = "2025-12-29T11:47:31.829Z" },
- { url = "https://files.pythonhosted.org/packages/00/8d/5f557be149931ef7d692b66296a44263a1769070466eb1e63d6d1b3b97c1/pygit2-1.19.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d8442ad863be83be86baff006a6e11de3cddf17c7ee77eac2d389765987b554", size = 5841500, upload-time = "2025-12-29T11:47:33.636Z" },
- { url = "https://files.pythonhosted.org/packages/ce/f7/0101b3058e64df334c48193dfd6f1493a24b0c7813382c6b2e4db7a09ffa/pygit2-1.19.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ae9c775be518c7f20bf340091d329d3b9203cbd4273bf1b5505dc82dccf08147", size = 6087805, upload-time = "2025-12-29T11:47:34.926Z" },
- { url = "https://files.pythonhosted.org/packages/60/26/7d3fa88362b1703cd5b9bde411f37cded3b1f99dc83b720fc0c65ac8f37b/pygit2-1.19.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d5a45d466a4bc5d9eb0619ffc26b17e4018285e35ba9e2fe39576f13480b63bc", size = 5809156, upload-time = "2025-12-29T11:47:36.396Z" },
- { url = "https://files.pythonhosted.org/packages/90/38/f1952af3f61b3a7a49c417ffb67a5140c1183e6b04ec714c8941937860bf/pygit2-1.19.1-cp314-cp314t-win32.whl", hash = "sha256:6621acaaf2670e8fd0727c15271e5209a99769b127300ef7fc56b49babc8b1c1", size = 969317, upload-time = "2025-12-29T11:47:38.01Z" },
- { url = "https://files.pythonhosted.org/packages/31/02/205a4d10cb1195f6abf0a509883ede90caddefca6d9c3b54ef96e79e8e8a/pygit2-1.19.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4418dea6936fe3c1a9375d7cd31a69e72997e645e588ed31c40d785c71adde35", size = 1197068, upload-time = "2025-12-29T11:47:39.065Z" },
- { url = "https://files.pythonhosted.org/packages/00/20/4571edf9bebc9d60dcf5d5c3cd0a12e55a79b91b02ef960c44e4ffc24c70/pygit2-1.19.1-cp314-cp314t-win_arm64.whl", hash = "sha256:3cbb8ab952224c0b305aa56f8759bcad5d9a9de885b00fe0ff8bed9ac365472e", size = 995635, upload-time = "2025-12-29T11:47:40.327Z" },
{ url = "https://files.pythonhosted.org/packages/45/01/607b8a400ffe46340df083d67cb05296f90e0d302d09addac5dc1afee47f/pygit2-1.19.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3c56ef9ac89e020ca005a39db4e045792b1ce98c2450a53f79815e9d831c006a", size = 5646594, upload-time = "2025-12-29T11:47:41.437Z" },
{ url = "https://files.pythonhosted.org/packages/18/59/45e517b86692120fd927b8949916203c50ffce0cd7a7124131d90d816fde/pygit2-1.19.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a6d89079f3af32f25abb8680eabea31143a4f02f3d1da6644c296ba89b6a2fc", size = 5644506, upload-time = "2025-12-29T11:47:42.779Z" },
{ url = "https://files.pythonhosted.org/packages/db/25/41c0c37c0f8b23677364d9f82ddbb1377d2342666045d39b508acc3d6f97/pygit2-1.19.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bfd44dc6f1d5b1165cc2097c39000c4a5cc05443d27a3a5f2791ad338f52b07", size = 5559864, upload-time = "2025-12-29T11:47:44.399Z" },
@@ -2098,7 +1347,7 @@ version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
- { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+ { name = "typing-extensions" },
]
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 = [
@@ -2153,36 +1402,21 @@ wheels = [
[[package]]
name = "pytokens"
-version = "0.4.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e5/16/4b9cfd90d55e66ffdb277d7ebe3bc25250c2311336ec3fc73b2673c794d5/pytokens-0.4.0.tar.gz", hash = "sha256:6b0b03e6ea7c9f9d47c5c61164b69ad30f4f0d70a5d9fe7eac4d19f24f77af2d", size = 15039, upload-time = "2026-01-19T07:59:50.623Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b4/05/3196399a353dd4cd99138a88f662810979ee2f1a1cdb0b417cb2f4507836/pytokens-0.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:92eb3ef88f27c22dc9dbab966ace4d61f6826e02ba04dac8e2d65ea31df56c8e", size = 160075, upload-time = "2026-01-19T07:59:00.316Z" },
- { url = "https://files.pythonhosted.org/packages/28/1d/c8fc4ed0a1c4f660391b201cda00b1d5bbcc00e2998e8bcd48b15eefd708/pytokens-0.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4b77858a680635ee9904306f54b0ee4781effb89e211ba0a773d76539537165", size = 247318, upload-time = "2026-01-19T07:59:01.636Z" },
- { url = "https://files.pythonhosted.org/packages/8e/0e/53e55ba01f3e858d229cd84b02481542f42ba59050483a78bf2447ee1af7/pytokens-0.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25cacc20c2ad90acb56f3739d87905473c54ca1fa5967ffcd675463fe965865e", size = 259752, upload-time = "2026-01-19T07:59:04.229Z" },
- { url = "https://files.pythonhosted.org/packages/dc/56/2d930d7f899e3f21868ca6e8ec739ac31e8fc532f66e09cbe45d3df0a84f/pytokens-0.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:628fab535ebc9079e4db35cd63cb401901c7ce8720a9834f9ad44b9eb4e0f1d4", size = 262842, upload-time = "2026-01-19T07:59:06.14Z" },
- { url = "https://files.pythonhosted.org/packages/42/dd/4e7e6920d23deffaf66e6f40d45f7610dcbc132ca5d90ab4faccef22f624/pytokens-0.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:4d0f568d7e82b7e96be56d03b5081de40e43c904eb6492bf09aaca47cd55f35b", size = 102620, upload-time = "2026-01-19T07:59:07.839Z" },
- { url = "https://files.pythonhosted.org/packages/3d/65/65460ebbfefd0bc1b160457904370d44f269e6e4582e0a9b6cba7c267b04/pytokens-0.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cd8da894e5a29ba6b6da8be06a4f7589d7220c099b5e363cb0643234b9b38c2a", size = 159864, upload-time = "2026-01-19T07:59:08.908Z" },
- { url = "https://files.pythonhosted.org/packages/25/70/a46669ec55876c392036b4da9808b5c3b1c5870bbca3d4cc923bf68bdbc1/pytokens-0.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:237ba7cfb677dbd3b01b09860810aceb448871150566b93cd24501d5734a04b1", size = 254448, upload-time = "2026-01-19T07:59:10.594Z" },
- { url = "https://files.pythonhosted.org/packages/62/0b/c486fc61299c2fc3b7f88ee4e115d4c8b6ffd1a7f88dc94b398b5b1bc4b8/pytokens-0.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01d1a61e36812e4e971cfe2c0e4c1f2d66d8311031dac8bf168af8a249fa04dd", size = 268863, upload-time = "2026-01-19T07:59:12.31Z" },
- { url = "https://files.pythonhosted.org/packages/79/92/b036af846707d25feaff7cafbd5280f1bd6a1034c16bb06a7c910209c1ab/pytokens-0.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e47e2ef3ec6ee86909e520d79f965f9b23389fda47460303cf715d510a6fe544", size = 267181, upload-time = "2026-01-19T07:59:13.856Z" },
- { url = "https://files.pythonhosted.org/packages/0d/c0/6d011fc00fefa74ce34816c84a923d2dd7c46b8dbc6ee52d13419786834c/pytokens-0.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d36954aba4557fd5a418a03cf595ecbb1cdcce119f91a49b19ef09d691a22ae", size = 102814, upload-time = "2026-01-19T07:59:15.288Z" },
- { url = "https://files.pythonhosted.org/packages/98/63/627b7e71d557383da5a97f473ad50f8d9c2c1f55c7d3c2531a120c796f6e/pytokens-0.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73eff3bdd8ad08da679867992782568db0529b887bed4c85694f84cdf35eafc6", size = 159744, upload-time = "2026-01-19T07:59:16.88Z" },
- { url = "https://files.pythonhosted.org/packages/28/d7/16f434c37ec3824eba6bcb6e798e5381a8dc83af7a1eda0f95c16fe3ade5/pytokens-0.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d97cc1f91b1a8e8ebccf31c367f28225699bea26592df27141deade771ed0afb", size = 253207, upload-time = "2026-01-19T07:59:18.069Z" },
- { url = "https://files.pythonhosted.org/packages/ab/96/04102856b9527701ae57d74a6393d1aca5bad18a1b1ca48ccffb3c93b392/pytokens-0.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c8952c537cb73a1a74369501a83b7f9d208c3cf92c41dd88a17814e68d48ce", size = 267452, upload-time = "2026-01-19T07:59:19.328Z" },
- { url = "https://files.pythonhosted.org/packages/0e/ef/0936eb472b89ab2d2c2c24bb81c50417e803fa89c731930d9fb01176fe9f/pytokens-0.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5dbf56f3c748aed9310b310d5b8b14e2c96d3ad682ad5a943f381bdbbdddf753", size = 265965, upload-time = "2026-01-19T07:59:20.613Z" },
- { url = "https://files.pythonhosted.org/packages/ae/f5/64f3d6f7df4a9e92ebda35ee85061f6260e16eac82df9396020eebbca775/pytokens-0.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:e131804513597f2dff2b18f9911d9b6276e21ef3699abeffc1c087c65a3d975e", size = 102813, upload-time = "2026-01-19T07:59:22.012Z" },
- { url = "https://files.pythonhosted.org/packages/5f/f1/d07e6209f18ef378fc2ae9dee8d1dfe91fd2447c2e2dbfa32867b6dd30cf/pytokens-0.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0d7374c917197106d3c4761374718bc55ea2e9ac0fb94171588ef5840ee1f016", size = 159968, upload-time = "2026-01-19T07:59:23.07Z" },
- { url = "https://files.pythonhosted.org/packages/0a/73/0eb111400abd382a04f253b269819db9fcc748aa40748441cebdcb6d068f/pytokens-0.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cd3fa1caf9e47a72ee134a29ca6b5bea84712724bba165d6628baa190c6ea5b", size = 253373, upload-time = "2026-01-19T07:59:24.381Z" },
- { url = "https://files.pythonhosted.org/packages/bd/8d/9e4e2fdb5bcaba679e54afcc304e9f13f488eb4d626e6b613f9553e03dbd/pytokens-0.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c6986576b7b07fe9791854caa5347923005a80b079d45b63b0be70d50cce5f1", size = 267024, upload-time = "2026-01-19T07:59:25.74Z" },
- { url = "https://files.pythonhosted.org/packages/cb/b7/e0a370321af2deb772cff14ff337e1140d1eac2c29a8876bfee995f486f0/pytokens-0.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9940f7c2e2f54fb1cb5fe17d0803c54da7a2bf62222704eb4217433664a186a7", size = 270912, upload-time = "2026-01-19T07:59:27.072Z" },
- { url = "https://files.pythonhosted.org/packages/7c/54/4348f916c440d4c3e68b53b4ed0e66b292d119e799fa07afa159566dcc86/pytokens-0.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:54691cf8f299e7efabcc25adb4ce715d3cef1491e1c930eaf555182f898ef66a", size = 103836, upload-time = "2026-01-19T07:59:28.112Z" },
- { url = "https://files.pythonhosted.org/packages/e8/f8/a693c0cfa9c783a2a8c4500b7b2a8bab420f8ca4f2d496153226bf1c12e3/pytokens-0.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:94ff5db97a0d3cd7248a5b07ba2167bd3edc1db92f76c6db00137bbaf068ddf8", size = 167643, upload-time = "2026-01-19T07:59:29.292Z" },
- { url = "https://files.pythonhosted.org/packages/c0/dd/a64eb1e9f3ec277b69b33ef1b40ffbcc8f0a3bafcde120997efc7bdefebf/pytokens-0.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d0dd6261cd9cc95fae1227b1b6ebee023a5fd4a4b6330b071c73a516f5f59b63", size = 289553, upload-time = "2026-01-19T07:59:30.537Z" },
- { url = "https://files.pythonhosted.org/packages/df/22/06c1079d93dbc3bca5d013e1795f3d8b9ed6c87290acd6913c1c526a6bb2/pytokens-0.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cdca8159df407dbd669145af4171a0d967006e0be25f3b520896bc7068f02c4", size = 302490, upload-time = "2026-01-19T07:59:32.352Z" },
- { url = "https://files.pythonhosted.org/packages/8d/de/a6f5e43115b4fbf4b93aa87d6c83c79932cdb084f9711daae04549e1e4ad/pytokens-0.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4b5770abeb2a24347380a1164a558f0ebe06e98aedbd54c45f7929527a5fb26e", size = 305652, upload-time = "2026-01-19T07:59:33.685Z" },
- { url = "https://files.pythonhosted.org/packages/ab/3d/c136e057cb622e36e0c3ff7a8aaa19ff9720050c4078235691da885fe6ee/pytokens-0.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:74500d72c561dad14c037a9e86a657afd63e277dd5a3bb7570932ab7a3b12551", size = 115472, upload-time = "2026-01-19T07:59:34.734Z" },
- { url = "https://files.pythonhosted.org/packages/7c/3c/6941a82f4f130af6e1c68c076b6789069ef10c04559bd4733650f902fd3b/pytokens-0.4.0-py3-none-any.whl", hash = "sha256:0508d11b4de157ee12063901603be87fb0253e8f4cb9305eb168b1202ab92068", size = 13224, upload-time = "2026-01-19T07:59:49.822Z" },
+version = "0.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" },
+ { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" },
+ { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" },
+ { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" },
+ { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" },
]
[[package]]
@@ -2210,58 +1444,6 @@ wheels = [
{ 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-ft"
-version = "8.0.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/5e/eb/5a0d575de784f9a1f94e2b1288c6886f13f34185e13117ed530f32b6f8a8/pyyaml_ft-8.0.0.tar.gz", hash = "sha256:0c947dce03954c7b5d38869ed4878b2e6ff1d44b08a0d84dc83fdad205ae39ab", size = 141057, upload-time = "2025-06-10T15:32:15.613Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/68/ba/a067369fe61a2e57fb38732562927d5bae088c73cb9bb5438736a9555b29/pyyaml_ft-8.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c1306282bc958bfda31237f900eb52c9bedf9b93a11f82e1aab004c9a5657a6", size = 187027, upload-time = "2025-06-10T15:31:48.722Z" },
- { url = "https://files.pythonhosted.org/packages/ad/c5/a3d2020ce5ccfc6aede0d45bcb870298652ac0cf199f67714d250e0cdf39/pyyaml_ft-8.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30c5f1751625786c19de751e3130fc345ebcba6a86f6bddd6e1285342f4bbb69", size = 176146, upload-time = "2025-06-10T15:31:50.584Z" },
- { url = "https://files.pythonhosted.org/packages/e3/bb/23a9739291086ca0d3189eac7cd92b4d00e9fdc77d722ab610c35f9a82ba/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fa992481155ddda2e303fcc74c79c05eddcdbc907b888d3d9ce3ff3e2adcfb0", size = 746792, upload-time = "2025-06-10T15:31:52.304Z" },
- { url = "https://files.pythonhosted.org/packages/5f/c2/e8825f4ff725b7e560d62a3609e31d735318068e1079539ebfde397ea03e/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cec6c92b4207004b62dfad1f0be321c9f04725e0f271c16247d8b39c3bf3ea42", size = 786772, upload-time = "2025-06-10T15:31:54.712Z" },
- { url = "https://files.pythonhosted.org/packages/35/be/58a4dcae8854f2fdca9b28d9495298fd5571a50d8430b1c3033ec95d2d0e/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06237267dbcab70d4c0e9436d8f719f04a51123f0ca2694c00dd4b68c338e40b", size = 778723, upload-time = "2025-06-10T15:31:56.093Z" },
- { url = "https://files.pythonhosted.org/packages/86/ed/fed0da92b5d5d7340a082e3802d84c6dc9d5fa142954404c41a544c1cb92/pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a7f332bc565817644cdb38ffe4739e44c3e18c55793f75dddb87630f03fc254", size = 758478, upload-time = "2025-06-10T15:31:58.314Z" },
- { url = "https://files.pythonhosted.org/packages/f0/69/ac02afe286275980ecb2dcdc0156617389b7e0c0a3fcdedf155c67be2b80/pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7d10175a746be65f6feb86224df5d6bc5c049ebf52b89a88cf1cd78af5a367a8", size = 799159, upload-time = "2025-06-10T15:31:59.675Z" },
- { url = "https://files.pythonhosted.org/packages/4e/ac/c492a9da2e39abdff4c3094ec54acac9747743f36428281fb186a03fab76/pyyaml_ft-8.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:58e1015098cf8d8aec82f360789c16283b88ca670fe4275ef6c48c5e30b22a96", size = 158779, upload-time = "2025-06-10T15:32:01.029Z" },
- { url = "https://files.pythonhosted.org/packages/5d/9b/41998df3298960d7c67653669f37710fa2d568a5fc933ea24a6df60acaf6/pyyaml_ft-8.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5f3e2ceb790d50602b2fd4ec37abbd760a8c778e46354df647e7c5a4ebb", size = 191331, upload-time = "2025-06-10T15:32:02.602Z" },
- { url = "https://files.pythonhosted.org/packages/0f/16/2710c252ee04cbd74d9562ebba709e5a284faeb8ada88fcda548c9191b47/pyyaml_ft-8.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d445bf6ea16bb93c37b42fdacfb2f94c8e92a79ba9e12768c96ecde867046d1", size = 182879, upload-time = "2025-06-10T15:32:04.466Z" },
- { url = "https://files.pythonhosted.org/packages/9a/40/ae8163519d937fa7bfa457b6f78439cc6831a7c2b170e4f612f7eda71815/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c56bb46b4fda34cbb92a9446a841da3982cdde6ea13de3fbd80db7eeeab8b49", size = 811277, upload-time = "2025-06-10T15:32:06.214Z" },
- { url = "https://files.pythonhosted.org/packages/f9/66/28d82dbff7f87b96f0eeac79b7d972a96b4980c1e445eb6a857ba91eda00/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dab0abb46eb1780da486f022dce034b952c8ae40753627b27a626d803926483b", size = 831650, upload-time = "2025-06-10T15:32:08.076Z" },
- { url = "https://files.pythonhosted.org/packages/e8/df/161c4566facac7d75a9e182295c223060373d4116dead9cc53a265de60b9/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd48d639cab5ca50ad957b6dd632c7dd3ac02a1abe0e8196a3c24a52f5db3f7a", size = 815755, upload-time = "2025-06-10T15:32:09.435Z" },
- { url = "https://files.pythonhosted.org/packages/05/10/f42c48fa5153204f42eaa945e8d1fd7c10d6296841dcb2447bf7da1be5c4/pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:052561b89d5b2a8e1289f326d060e794c21fa068aa11255fe71d65baf18a632e", size = 810403, upload-time = "2025-06-10T15:32:11.051Z" },
- { url = "https://files.pythonhosted.org/packages/d5/d2/e369064aa51009eb9245399fd8ad2c562bd0bcd392a00be44b2a824ded7c/pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3bb4b927929b0cb162fb1605392a321e3333e48ce616cdcfa04a839271373255", size = 835581, upload-time = "2025-06-10T15:32:12.897Z" },
- { url = "https://files.pythonhosted.org/packages/c0/28/26534bed77109632a956977f60d8519049f545abc39215d086e33a61f1f2/pyyaml_ft-8.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:de04cfe9439565e32f178106c51dd6ca61afaa2907d143835d501d84703d3793", size = 171579, upload-time = "2025-06-10T15:32:14.34Z" },
]
[[package]]
@@ -2302,70 +1484,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/77/69/c50a63842b6bd48850ebc7ab22d46e7a2a32d824ad6c605b218441814639/regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913", size = 266279, upload-time = "2026-01-14T23:15:07.678Z" },
{ url = "https://files.pythonhosted.org/packages/f2/36/39d0b29d087e2b11fd8191e15e81cce1b635fcc845297c67f11d0d19274d/regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a", size = 277166, upload-time = "2026-01-14T23:15:09.257Z" },
{ url = "https://files.pythonhosted.org/packages/28/32/5b8e476a12262748851fa8ab1b0be540360692325975b094e594dfebbb52/regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056", size = 270415, upload-time = "2026-01-14T23:15:10.743Z" },
- { url = "https://files.pythonhosted.org/packages/f8/2e/6870bb16e982669b674cce3ee9ff2d1d46ab80528ee6bcc20fb2292efb60/regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e", size = 489164, upload-time = "2026-01-14T23:15:13.962Z" },
- { url = "https://files.pythonhosted.org/packages/dc/67/9774542e203849b0286badf67199970a44ebdb0cc5fb739f06e47ada72f8/regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10", size = 291218, upload-time = "2026-01-14T23:15:15.647Z" },
- { url = "https://files.pythonhosted.org/packages/b2/87/b0cda79f22b8dee05f774922a214da109f9a4c0eca5da2c9d72d77ea062c/regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc", size = 288895, upload-time = "2026-01-14T23:15:17.788Z" },
- { url = "https://files.pythonhosted.org/packages/3b/6a/0041f0a2170d32be01ab981d6346c83a8934277d82c780d60b127331f264/regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599", size = 798680, upload-time = "2026-01-14T23:15:19.342Z" },
- { url = "https://files.pythonhosted.org/packages/58/de/30e1cfcdbe3e891324aa7568b7c968771f82190df5524fabc1138cb2d45a/regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae", size = 864210, upload-time = "2026-01-14T23:15:22.005Z" },
- { url = "https://files.pythonhosted.org/packages/64/44/4db2f5c5ca0ccd40ff052ae7b1e9731352fcdad946c2b812285a7505ca75/regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5", size = 912358, upload-time = "2026-01-14T23:15:24.569Z" },
- { url = "https://files.pythonhosted.org/packages/79/b6/e6a5665d43a7c42467138c8a2549be432bad22cbd206f5ec87162de74bd7/regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6", size = 803583, upload-time = "2026-01-14T23:15:26.526Z" },
- { url = "https://files.pythonhosted.org/packages/e7/53/7cd478222169d85d74d7437e74750005e993f52f335f7c04ff7adfda3310/regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788", size = 775782, upload-time = "2026-01-14T23:15:29.352Z" },
- { url = "https://files.pythonhosted.org/packages/ca/b5/75f9a9ee4b03a7c009fe60500fe550b45df94f0955ca29af16333ef557c5/regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714", size = 787978, upload-time = "2026-01-14T23:15:31.295Z" },
- { url = "https://files.pythonhosted.org/packages/72/b3/79821c826245bbe9ccbb54f6eadb7879c722fd3e0248c17bfc90bf54e123/regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d", size = 858550, upload-time = "2026-01-14T23:15:33.558Z" },
- { url = "https://files.pythonhosted.org/packages/4a/85/2ab5f77a1c465745bfbfcb3ad63178a58337ae8d5274315e2cc623a822fa/regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3", size = 763747, upload-time = "2026-01-14T23:15:35.206Z" },
- { url = "https://files.pythonhosted.org/packages/6d/84/c27df502d4bfe2873a3e3a7cf1bdb2b9cc10284d1a44797cf38bed790470/regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31", size = 850615, upload-time = "2026-01-14T23:15:37.523Z" },
- { url = "https://files.pythonhosted.org/packages/7d/b7/658a9782fb253680aa8ecb5ccbb51f69e088ed48142c46d9f0c99b46c575/regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3", size = 789951, upload-time = "2026-01-14T23:15:39.582Z" },
- { url = "https://files.pythonhosted.org/packages/fc/2a/5928af114441e059f15b2f63e188bd00c6529b3051c974ade7444b85fcda/regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f", size = 266275, upload-time = "2026-01-14T23:15:42.108Z" },
- { url = "https://files.pythonhosted.org/packages/4f/16/5bfbb89e435897bff28cf0352a992ca719d9e55ebf8b629203c96b6ce4f7/regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e", size = 277145, upload-time = "2026-01-14T23:15:44.244Z" },
- { url = "https://files.pythonhosted.org/packages/56/c1/a09ff7392ef4233296e821aec5f78c51be5e91ffde0d163059e50fd75835/regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337", size = 270411, upload-time = "2026-01-14T23:15:45.858Z" },
- { url = "https://files.pythonhosted.org/packages/3c/38/0cfd5a78e5c6db00e6782fdae70458f89850ce95baa5e8694ab91d89744f/regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be", size = 492068, upload-time = "2026-01-14T23:15:47.616Z" },
- { url = "https://files.pythonhosted.org/packages/50/72/6c86acff16cb7c959c4355826bbf06aad670682d07c8f3998d9ef4fee7cd/regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8", size = 292756, upload-time = "2026-01-14T23:15:49.307Z" },
- { url = "https://files.pythonhosted.org/packages/4e/58/df7fb69eadfe76526ddfce28abdc0af09ffe65f20c2c90932e89d705153f/regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd", size = 291114, upload-time = "2026-01-14T23:15:51.484Z" },
- { url = "https://files.pythonhosted.org/packages/ed/6c/a4011cd1cf96b90d2cdc7e156f91efbd26531e822a7fbb82a43c1016678e/regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a", size = 807524, upload-time = "2026-01-14T23:15:53.102Z" },
- { url = "https://files.pythonhosted.org/packages/1d/25/a53ffb73183f69c3e9f4355c4922b76d2840aee160af6af5fac229b6201d/regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93", size = 873455, upload-time = "2026-01-14T23:15:54.956Z" },
- { url = "https://files.pythonhosted.org/packages/66/0b/8b47fc2e8f97d9b4a851736f3890a5f786443aa8901061c55f24c955f45b/regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af", size = 915007, upload-time = "2026-01-14T23:15:57.041Z" },
- { url = "https://files.pythonhosted.org/packages/c2/fa/97de0d681e6d26fabe71968dbee06dd52819e9a22fdce5dac7256c31ed84/regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09", size = 812794, upload-time = "2026-01-14T23:15:58.916Z" },
- { url = "https://files.pythonhosted.org/packages/22/38/e752f94e860d429654aa2b1c51880bff8dfe8f084268258adf9151cf1f53/regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5", size = 781159, upload-time = "2026-01-14T23:16:00.817Z" },
- { url = "https://files.pythonhosted.org/packages/e9/a7/d739ffaef33c378fc888302a018d7f81080393d96c476b058b8c64fd2b0d/regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794", size = 795558, upload-time = "2026-01-14T23:16:03.267Z" },
- { url = "https://files.pythonhosted.org/packages/3e/c4/542876f9a0ac576100fc73e9c75b779f5c31e3527576cfc9cb3009dcc58a/regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a", size = 868427, upload-time = "2026-01-14T23:16:05.646Z" },
- { url = "https://files.pythonhosted.org/packages/fc/0f/d5655bea5b22069e32ae85a947aa564912f23758e112cdb74212848a1a1b/regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80", size = 769939, upload-time = "2026-01-14T23:16:07.542Z" },
- { url = "https://files.pythonhosted.org/packages/20/06/7e18a4fa9d326daeda46d471a44ef94201c46eaa26dbbb780b5d92cbfdda/regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2", size = 854753, upload-time = "2026-01-14T23:16:10.395Z" },
- { url = "https://files.pythonhosted.org/packages/3b/67/dc8946ef3965e166f558ef3b47f492bc364e96a265eb4a2bb3ca765c8e46/regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60", size = 799559, upload-time = "2026-01-14T23:16:12.347Z" },
- { url = "https://files.pythonhosted.org/packages/a5/61/1bba81ff6d50c86c65d9fd84ce9699dd106438ee4cdb105bf60374ee8412/regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952", size = 268879, upload-time = "2026-01-14T23:16:14.049Z" },
- { url = "https://files.pythonhosted.org/packages/e9/5e/cef7d4c5fb0ea3ac5c775fd37db5747f7378b29526cc83f572198924ff47/regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10", size = 280317, upload-time = "2026-01-14T23:16:15.718Z" },
- { url = "https://files.pythonhosted.org/packages/b4/52/4317f7a5988544e34ab57b4bde0f04944c4786128c933fb09825924d3e82/regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829", size = 271551, upload-time = "2026-01-14T23:16:17.533Z" },
- { url = "https://files.pythonhosted.org/packages/52/0a/47fa888ec7cbbc7d62c5f2a6a888878e76169170ead271a35239edd8f0e8/regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac", size = 489170, upload-time = "2026-01-14T23:16:19.835Z" },
- { url = "https://files.pythonhosted.org/packages/ac/c4/d000e9b7296c15737c9301708e9e7fbdea009f8e93541b6b43bdb8219646/regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6", size = 291146, upload-time = "2026-01-14T23:16:21.541Z" },
- { url = "https://files.pythonhosted.org/packages/f9/b6/921cc61982e538682bdf3bdf5b2c6ab6b34368da1f8e98a6c1ddc503c9cf/regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2", size = 288986, upload-time = "2026-01-14T23:16:23.381Z" },
- { url = "https://files.pythonhosted.org/packages/ca/33/eb7383dde0bbc93f4fb9d03453aab97e18ad4024ac7e26cef8d1f0a2cff0/regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846", size = 799098, upload-time = "2026-01-14T23:16:25.088Z" },
- { url = "https://files.pythonhosted.org/packages/27/56/b664dccae898fc8d8b4c23accd853f723bde0f026c747b6f6262b688029c/regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b", size = 864980, upload-time = "2026-01-14T23:16:27.297Z" },
- { url = "https://files.pythonhosted.org/packages/16/40/0999e064a170eddd237bae9ccfcd8f28b3aa98a38bf727a086425542a4fc/regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e", size = 911607, upload-time = "2026-01-14T23:16:29.235Z" },
- { url = "https://files.pythonhosted.org/packages/07/78/c77f644b68ab054e5a674fb4da40ff7bffb2c88df58afa82dbf86573092d/regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde", size = 803358, upload-time = "2026-01-14T23:16:31.369Z" },
- { url = "https://files.pythonhosted.org/packages/27/31/d4292ea8566eaa551fafc07797961c5963cf5235c797cc2ae19b85dfd04d/regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5", size = 775833, upload-time = "2026-01-14T23:16:33.141Z" },
- { url = "https://files.pythonhosted.org/packages/ce/b2/cff3bf2fea4133aa6fb0d1e370b37544d18c8350a2fa118c7e11d1db0e14/regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34", size = 788045, upload-time = "2026-01-14T23:16:35.005Z" },
- { url = "https://files.pythonhosted.org/packages/8d/99/2cb9b69045372ec877b6f5124bda4eb4253bc58b8fe5848c973f752bc52c/regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75", size = 859374, upload-time = "2026-01-14T23:16:36.919Z" },
- { url = "https://files.pythonhosted.org/packages/09/16/710b0a5abe8e077b1729a562d2f297224ad079f3a66dce46844c193416c8/regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e", size = 763940, upload-time = "2026-01-14T23:16:38.685Z" },
- { url = "https://files.pythonhosted.org/packages/dd/d1/7585c8e744e40eb3d32f119191969b91de04c073fca98ec14299041f6e7e/regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160", size = 850112, upload-time = "2026-01-14T23:16:40.646Z" },
- { url = "https://files.pythonhosted.org/packages/af/d6/43e1dd85df86c49a347aa57c1f69d12c652c7b60e37ec162e3096194a278/regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1", size = 789586, upload-time = "2026-01-14T23:16:42.799Z" },
- { url = "https://files.pythonhosted.org/packages/93/38/77142422f631e013f316aaae83234c629555729a9fbc952b8a63ac91462a/regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1", size = 271691, upload-time = "2026-01-14T23:16:44.671Z" },
- { url = "https://files.pythonhosted.org/packages/4a/a9/ab16b4649524ca9e05213c1cdbb7faa85cc2aa90a0230d2f796cbaf22736/regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903", size = 280422, upload-time = "2026-01-14T23:16:46.607Z" },
- { url = "https://files.pythonhosted.org/packages/be/2a/20fd057bf3521cb4791f69f869635f73e0aaf2b9ad2d260f728144f9047c/regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705", size = 273467, upload-time = "2026-01-14T23:16:48.967Z" },
- { url = "https://files.pythonhosted.org/packages/ad/77/0b1e81857060b92b9cad239104c46507dd481b3ff1fa79f8e7f865aae38a/regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8", size = 492073, upload-time = "2026-01-14T23:16:51.154Z" },
- { url = "https://files.pythonhosted.org/packages/70/f3/f8302b0c208b22c1e4f423147e1913fd475ddd6230565b299925353de644/regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf", size = 292757, upload-time = "2026-01-14T23:16:53.08Z" },
- { url = "https://files.pythonhosted.org/packages/bf/f0/ef55de2460f3b4a6da9d9e7daacd0cb79d4ef75c64a2af316e68447f0df0/regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d", size = 291122, upload-time = "2026-01-14T23:16:55.383Z" },
- { url = "https://files.pythonhosted.org/packages/cf/55/bb8ccbacabbc3a11d863ee62a9f18b160a83084ea95cdfc5d207bfc3dd75/regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84", size = 807761, upload-time = "2026-01-14T23:16:57.251Z" },
- { url = "https://files.pythonhosted.org/packages/8f/84/f75d937f17f81e55679a0509e86176e29caa7298c38bd1db7ce9c0bf6075/regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df", size = 873538, upload-time = "2026-01-14T23:16:59.349Z" },
- { url = "https://files.pythonhosted.org/packages/b8/d9/0da86327df70349aa8d86390da91171bd3ca4f0e7c1d1d453a9c10344da3/regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434", size = 915066, upload-time = "2026-01-14T23:17:01.607Z" },
- { url = "https://files.pythonhosted.org/packages/2a/5e/f660fb23fc77baa2a61aa1f1fe3a4eea2bbb8a286ddec148030672e18834/regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a", size = 812938, upload-time = "2026-01-14T23:17:04.366Z" },
- { url = "https://files.pythonhosted.org/packages/69/33/a47a29bfecebbbfd1e5cd3f26b28020a97e4820f1c5148e66e3b7d4b4992/regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10", size = 781314, upload-time = "2026-01-14T23:17:06.378Z" },
- { url = "https://files.pythonhosted.org/packages/65/ec/7ec2bbfd4c3f4e494a24dec4c6943a668e2030426b1b8b949a6462d2c17b/regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac", size = 795652, upload-time = "2026-01-14T23:17:08.521Z" },
- { url = "https://files.pythonhosted.org/packages/46/79/a5d8651ae131fe27d7c521ad300aa7f1c7be1dbeee4d446498af5411b8a9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea", size = 868550, upload-time = "2026-01-14T23:17:10.573Z" },
- { url = "https://files.pythonhosted.org/packages/06/b7/25635d2809664b79f183070786a5552dd4e627e5aedb0065f4e3cf8ee37d/regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e", size = 769981, upload-time = "2026-01-14T23:17:12.871Z" },
- { url = "https://files.pythonhosted.org/packages/16/8b/fc3fcbb2393dcfa4a6c5ffad92dc498e842df4581ea9d14309fcd3c55fb9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521", size = 854780, upload-time = "2026-01-14T23:17:14.837Z" },
- { url = "https://files.pythonhosted.org/packages/d0/38/dde117c76c624713c8a2842530be9c93ca8b606c0f6102d86e8cd1ce8bea/regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db", size = 799778, upload-time = "2026-01-14T23:17:17.369Z" },
- { url = "https://files.pythonhosted.org/packages/e3/0d/3a6cfa9ae99606afb612d8fb7a66b245a9d5ff0f29bb347c8a30b6ad561b/regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e", size = 274667, upload-time = "2026-01-14T23:17:19.301Z" },
- { url = "https://files.pythonhosted.org/packages/5b/b2/297293bb0742fd06b8d8e2572db41a855cdf1cae0bf009b1cb74fe07e196/regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf", size = 284386, upload-time = "2026-01-14T23:17:21.231Z" },
- { url = "https://files.pythonhosted.org/packages/95/e4/a3b9480c78cf8ee86626cb06f8d931d74d775897d44201ccb813097ae697/regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70", size = 274837, upload-time = "2026-01-14T23:17:23.146Z" },
]
[[package]]
@@ -2396,16 +1514,30 @@ wheels = [
]
[[package]]
+name = "responses"
+version = "0.25.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyyaml" },
+ { name = "requests" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0e/95/89c054ad70bfef6da605338b009b2e283485835351a9935c7bfbfaca7ffc/responses-0.25.8.tar.gz", hash = "sha256:9374d047a575c8f781b94454db5cab590b6029505f488d12899ddb10a4af1cf4", size = 79320, upload-time = "2025-08-08T19:01:46.709Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1c/4c/cc276ce57e572c102d9542d383b2cfd551276581dc60004cb94fe8774c11/responses-0.25.8-py3-none-any.whl", hash = "sha256:0c710af92def29c8352ceadff0c3fe340ace27cf5af1bbe46fb71275bcd2831c", size = 34769, upload-time = "2025-08-08T19:01:45.018Z" },
+]
+
+[[package]]
name = "rich"
-version = "14.3.1"
+version = "14.3.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/a1/84/4831f881aa6ff3c976f6d6809b58cdfa350593ffc0dc3c58f5f6586780fb/rich-14.3.1.tar.gz", hash = "sha256:b8c5f568a3a749f9290ec6bddedf835cec33696bfc1e48bcfecb276c7386e4b8", size = 230125, upload-time = "2026-01-24T21:40:44.847Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/74/99/a4cab2acbb884f80e558b0771e97e21e939c5dfb460f488d19df485e8298/rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8", size = 230143, upload-time = "2026-02-01T16:20:47.908Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/87/2a/a1810c8627b9ec8c57ec5ec325d306701ae7be50235e8fd81266e002a3cc/rich-14.3.1-py3-none-any.whl", hash = "sha256:da750b1aebbff0b372557426fb3f35ba56de8ef954b3190315eb64076d6fb54e", size = 309952, upload-time = "2026-01-24T21:40:42.969Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/45/615f5babd880b4bd7d405cc0dc348234c5ffb6ed1ea33e152ede08b2072d/rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69", size = 309963, upload-time = "2026-02-01T16:20:46.078Z" },
]
[[package]]
@@ -2522,34 +1654,6 @@ wheels = [
{ 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]]
@@ -2596,14 +1700,14 @@ wheels = [
[[package]]
name = "tqdm"
-version = "4.67.1"
+version = "4.67.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/27/89/4b0001b2dab8df0a5ee2787dcbe771de75ded01f18f1f8d53dedeea2882b/tqdm-4.67.2.tar.gz", hash = "sha256:649aac53964b2cb8dec76a14b405a4c0d13612cb8933aae547dd144eacc99653", size = 169514, upload-time = "2026-01-30T23:12:06.555Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/e2/31eac96de2915cf20ccaed0225035db149dfb9165a9ed28d4b252ef3f7f7/tqdm-4.67.2-py3-none-any.whl", hash = "sha256:9a12abcbbff58b6036b2167d9d3853042b9d436fe7330f06ae047867f2f8e0a7", size = 78354, upload-time = "2026-01-30T23:12:04.368Z" },
]
[[package]]
@@ -2632,14 +1736,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/0c/f8834a0d465afc4ea194d43fe10dd11e23874f5c102cec260172108a5cdc/typeid_python-0.3.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7dedb9894dea2a39653822224af6292710a54af0c9f48ba896bea6c22b6bff06", size = 236578, upload-time = "2026-01-28T18:23:22.874Z" },
{ url = "https://files.pythonhosted.org/packages/b1/29/c9842c47610213821cddbb274d093e36dc5d4346195eb5f036856f7d15f3/typeid_python-0.3.9-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:20ce022fe7d6be11a0d332d794e75a056b5aa225969f4b8ef9dbe4a94e651f2c", size = 270059, upload-time = "2026-01-28T18:23:24.396Z" },
{ url = "https://files.pythonhosted.org/packages/8b/87/9c3ae9936491f29ff09dcdbfdc4476368cc75b09be515ab31c5a2519ea7d/typeid_python-0.3.9-cp312-cp312-win_amd64.whl", hash = "sha256:bba27b8d1708e9654b85ffbcc4fc89a7c1d6cea65fbdab7fb9069d8cd71c2acd", size = 131062, upload-time = "2026-01-28T18:23:25.832Z" },
- { url = "https://files.pythonhosted.org/packages/37/d8/366968632ad71e09125761f5313289e001695424ae68847e5f6df1e22b7b/typeid_python-0.3.9-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:11521763da6308ce0119d5a145c51b0e9d22b04b93e151c3ba73d5a359ec714b", size = 240529, upload-time = "2026-01-28T18:23:27.555Z" },
- { url = "https://files.pythonhosted.org/packages/3c/43/dd71c7d5db5ca1bcf0f4c10771c45792655a4b4552cc4482d10a9fbc3838/typeid_python-0.3.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c8e32ba0b0da2f7520bc2d2b8301dfe56dc1a1094405751eb8a24d9a8b59c4a", size = 236828, upload-time = "2026-01-28T18:23:29.288Z" },
- { url = "https://files.pythonhosted.org/packages/cb/31/778a97d5f6dd0f0c4c8dd469a77be2bc8006ff76521bb27146a284ec0757/typeid_python-0.3.9-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:22c617e05319768e3cc8e0b4750238d4b1b5359746f0a0293e06dc4cd298bc80", size = 269743, upload-time = "2026-01-28T18:23:31.022Z" },
- { url = "https://files.pythonhosted.org/packages/d5/55/9891583b35aff55c9161005de8eb1444f928f1a4718e5a76fd8d86ec292d/typeid_python-0.3.9-cp313-cp313-win_amd64.whl", hash = "sha256:630cbcb17f3ac81ae346959cdf1ee7e69204e467bbfbf577fb6d98f38c769468", size = 130958, upload-time = "2026-01-28T18:23:32.358Z" },
- { url = "https://files.pythonhosted.org/packages/67/00/8c53af3d3859aeac99a6907018dfc118191bb05e08be4e4f2ddcab2cf5f6/typeid_python-0.3.9-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:4b88012e1ba62933f1b4161c0fb22e80b1047a45391cbe6ef1580ffebf95ed0a", size = 240337, upload-time = "2026-01-28T18:23:33.53Z" },
- { url = "https://files.pythonhosted.org/packages/ca/90/e451b06da5c28dc3ba7f08bd5d2cbe5dbd4b25e79c20eb60641ebb42c2c6/typeid_python-0.3.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:54e15462494f04c5bb3ea396ff65b3e1820eb80e9ffcd70575c3b2055dc4d3e9", size = 236451, upload-time = "2026-01-28T18:23:35.001Z" },
- { url = "https://files.pythonhosted.org/packages/87/fd/3929b7ce31298cd2ae716e6e991a248e24641e483f9d66b16755939d83ff/typeid_python-0.3.9-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:f2ae659428e88f382fadfb3801f9aa0d4c0bec5a67205570ef95ce0ca81ed5b8", size = 269235, upload-time = "2026-01-28T18:23:36.419Z" },
- { url = "https://files.pythonhosted.org/packages/cd/56/f1548a9f0c9c71a8d67150f50bc3a2ef36a74b89f060ab90685ef07ea859/typeid_python-0.3.9-cp314-cp314-win_amd64.whl", hash = "sha256:1fc43c61b228c281c099e7d68824a75202dd81427cdb4f411b93383d8350c428", size = 130948, upload-time = "2026-01-28T18:23:37.705Z" },
]
[[package]]
@@ -2731,58 +1827,105 @@ version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
+ { name = "anthropic" },
+ { name = "anyio" },
+ { name = "async-lru" },
+ { name = "attrs" },
+ { name = "boto3" },
+ { name = "cachetools" },
+ { name = "cattrs" },
{ name = "click" },
- { name = "imbue-core" },
- { name = "imbue-tools" },
+ { name = "diskcache" },
+ { name = "google-genai" },
+ { name = "groq" },
+ { name = "grpclib" },
+ { name = "httpx" },
+ { name = "inline-snapshot" },
{ name = "jinja2" },
+ { name = "libcst" },
{ name = "loguru" },
+ { name = "openai" },
+ { name = "pathspec" },
+ { name = "prometheus-client" },
+ { name = "psycopg", extra = ["binary"] },
{ name = "pydantic" },
+ { name = "pydantic-settings" },
+ { name = "pygit2" },
{ name = "pygments" },
+ { name = "pyhumps" },
+ { name = "pylint" },
{ name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-mock" },
+ { name = "python-gitlab" },
+ { name = "requests" },
{ name = "syrupy" },
+ { name = "tblib" },
+ { name = "tenacity" },
+ { name = "tiktoken" },
{ name = "together" },
- { name = "vet-types" },
+ { name = "toml" },
+ { name = "traceback-with-variables" },
+ { name = "typeid-python" },
+ { name = "yasoo" },
]
[package.dev-dependencies]
dev = [
{ name = "black" },
+ { name = "moto" },
]
[package.metadata]
requires-dist = [
{ name = "aiohttp", specifier = ">=3.8.0" },
+ { name = "anthropic", specifier = "~=0.54" },
+ { name = "anyio" },
+ { name = "async-lru" },
+ { name = "attrs" },
+ { name = "boto3", specifier = ">=1.38.27" },
+ { name = "cachetools" },
+ { name = "cattrs" },
{ name = "click" },
- { name = "imbue-core", editable = "imbue_core" },
- { name = "imbue-tools", editable = "imbue_tools" },
+ { name = "diskcache", specifier = ">=5.6.3" },
+ { name = "google-genai", specifier = ">=1.26.0" },
+ { name = "groq", specifier = ">=0.18.0" },
+ { name = "grpclib", specifier = ">=0.4.7" },
+ { name = "httpx" },
+ { name = "inline-snapshot" },
{ name = "jinja2" },
+ { name = "libcst" },
{ name = "loguru" },
- { name = "pydantic" },
+ { name = "openai", specifier = ">=1.79.0" },
+ { name = "pathspec" },
+ { name = "prometheus-client", specifier = ">=0.20.0" },
+ { name = "psycopg", extras = ["binary"] },
+ { name = "pydantic", specifier = ">=2.11.4" },
+ { name = "pydantic-settings" },
+ { name = "pygit2", specifier = ">=1.18.0" },
{ name = "pygments", specifier = ">=2.0.0" },
+ { name = "pyhumps" },
+ { name = "pylint", specifier = "==3.2.6" },
{ name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-mock" },
+ { name = "python-gitlab", specifier = ">=4.5.0" },
+ { name = "requests" },
{ name = "syrupy" },
+ { name = "tblib", specifier = "==2.0.0" },
+ { name = "tenacity", specifier = ">=8.2.2" },
+ { name = "tiktoken" },
{ name = "together", specifier = ">=1.5.35" },
- { name = "vet-types", editable = "vet_types" },
-]
-
-[package.metadata.requires-dev]
-dev = [{ name = "black" }]
-
-[[package]]
-name = "vet-types"
-version = "0.1.0"
-source = { editable = "vet_types" }
-dependencies = [
- { name = "imbue-core" },
- { name = "pydantic" },
+ { name = "toml" },
+ { name = "traceback-with-variables", specifier = ">=2.2.0" },
{ name = "typeid-python" },
+ { name = "yasoo" },
]
-[package.metadata]
-requires-dist = [
- { name = "imbue-core" },
- { name = "pydantic" },
- { name = "typeid-python" },
+[package.metadata.requires-dev]
+dev = [
+ { name = "black" },
+ { name = "moto", specifier = ">=4.1.12" },
]
[[package]]
@@ -2813,21 +1956,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" },
{ url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" },
{ url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" },
- { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
- { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
- { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
- { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
- { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
- { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
- { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
- { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
- { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
- { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
- { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
]
[[package]]
+name = "werkzeug"
+version = "3.1.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" },
+]
+
+[[package]]
name = "win32-setctime"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
@@ -2837,6 +1981,15 @@ wheels = [
]
[[package]]
+name = "xmltodict"
+version = "1.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6a/aa/917ceeed4dbb80d2f04dbd0c784b7ee7bba8ae5a54837ef0e5e062cd3cfb/xmltodict-1.0.2.tar.gz", hash = "sha256:54306780b7c2175a3967cad1db92f218207e5bc1aba697d887807c0fb68b7649", size = 25725, upload-time = "2025-09-17T21:59:26.459Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c0/20/69a0e6058bc5ea74892d089d64dfc3a62ba78917ec5e2cfa70f7c92ba3a5/xmltodict-1.0.2-py3-none-any.whl", hash = "sha256:62d0fddb0dcbc9f642745d8bbf4d81fd17d6dfaec5a15b5c1876300aad92af0d", size = 13893, upload-time = "2025-09-17T21:59:24.859Z" },
+]
+
+[[package]]
name = "yarl"
version = "1.22.0"
source = { registry = "https://pypi.org/simple" }
@@ -2879,70 +2032,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" },
{ url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" },
{ url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" },
- { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" },
- { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" },
- { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" },
- { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" },
- { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" },
- { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" },
- { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" },
- { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" },
- { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" },
- { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" },
- { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" },
- { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" },
- { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" },
- { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" },
- { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" },
- { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" },
- { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" },
- { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" },
- { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" },
- { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" },
- { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" },
- { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" },
- { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" },
- { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" },
- { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" },
- { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" },
- { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" },
- { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" },
- { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" },
- { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" },
- { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" },
- { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" },
- { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" },
- { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" },
- { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" },
- { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" },
- { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" },
- { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" },
- { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" },
- { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" },
- { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" },
- { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" },
- { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" },
- { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" },
- { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" },
- { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" },
- { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" },
- { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" },
- { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" },
- { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" },
- { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" },
- { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" },
- { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" },
- { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" },
- { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" },
- { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" },
- { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" },
- { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" },
- { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" },
- { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" },
- { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" },
- { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" },
- { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" },
- { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" },
{ url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" },
]
diff --git a/vet/api.py b/vet/api.py
@@ -8,17 +8,17 @@ from pathlib import Path
from loguru import logger
-from imbue_core.data_types import IdentifiedVerifyIssue
-from imbue_core.data_types import IssueIdentificationDebugInfo
-from vet_types.messages import ConversationMessageUnion
-from imbue_tools.get_conversation_history.get_conversation_history import (
+from vet.imbue_core.data_types import IdentifiedVerifyIssue
+from vet.imbue_core.data_types import IssueIdentificationDebugInfo
+from vet.vet_types.messages import ConversationMessageUnion
+from vet.imbue_tools.get_conversation_history.get_conversation_history import (
ConversationLoadingError,
)
-from imbue_tools.get_conversation_history.input_data_types import IdentifierInputs
-from imbue_tools.repo_utils.project_context import LazyProjectContext
-from imbue_tools.repo_utils.project_context import ProjectContext
-from imbue_tools.types.vet_config import VetConfig
-from imbue_tools.util_prompts.goal_from_conversation import get_goal_from_conversation
+from vet.imbue_tools.get_conversation_history.input_data_types import IdentifierInputs
+from vet.imbue_tools.repo_utils.project_context import LazyProjectContext
+from vet.imbue_tools.repo_utils.project_context import ProjectContext
+from vet.imbue_tools.types.vet_config import VetConfig
+from vet.imbue_tools.util_prompts.goal_from_conversation import get_goal_from_conversation
from vet.issue_identifiers import registry
from vet.issue_identifiers.utils import ReturnCapturingGenerator
from vet.repo_utils import VET_MAX_PROMPT_TOKENS
diff --git a/vet/cli/config/loader.py b/vet/cli/config/loader.py
@@ -6,9 +6,9 @@ from pathlib import Path
from pydantic import ValidationError
-from imbue_core.agents.configs import LanguageModelGenerationConfig
-from imbue_core.agents.configs import OpenAICompatibleModelConfig
-from imbue_core.agents.llm_apis.common import get_model_max_output_tokens
+from vet.imbue_core.agents.configs import LanguageModelGenerationConfig
+from vet.imbue_core.agents.configs import OpenAICompatibleModelConfig
+from vet.imbue_core.agents.llm_apis.common import get_model_max_output_tokens
from vet.cli.config.cli_config_schema import CliConfigPreset
from vet.cli.config.cli_config_schema import merge_presets
from vet.cli.config.cli_config_schema import parse_cli_config_from_dict
diff --git a/vet/cli/main.py b/vet/cli/main.py
@@ -11,11 +11,11 @@ from pathlib import Path
from loguru import logger
-from imbue_core.data_types import IssueCode
-from imbue_tools.get_conversation_history.get_conversation_history import (
+from vet.imbue_core.data_types import IssueCode
+from vet.imbue_tools.get_conversation_history.get_conversation_history import (
parse_conversation_history,
)
-from imbue_tools.types.vet_config import VetConfig
+from vet.imbue_tools.types.vet_config import VetConfig
from vet.api import find_issues
from vet.cli.config.cli_config_schema import CLI_DEFAULTS
from vet.cli.config.cli_config_schema import CliConfigPreset
diff --git a/vet/cli/models.py b/vet/cli/models.py
@@ -1,11 +1,11 @@
from __future__ import annotations
-from imbue_core.agents.llm_apis.anthropic_api import AnthropicModelName
-from imbue_core.agents.llm_apis.common import get_all_model_names
-from imbue_core.agents.llm_apis.gemini_api import GeminiModelName
-from imbue_core.agents.llm_apis.groq_api import GroqSupportedModelName
-from imbue_core.agents.llm_apis.openai_api import OpenAIModelName
-from imbue_core.agents.llm_apis.together_api import TogetherAIModelName
+from vet.imbue_core.agents.llm_apis.anthropic_api import AnthropicModelName
+from vet.imbue_core.agents.llm_apis.common import get_all_model_names
+from vet.imbue_core.agents.llm_apis.gemini_api import GeminiModelName
+from vet.imbue_core.agents.llm_apis.groq_api import GroqSupportedModelName
+from vet.imbue_core.agents.llm_apis.openai_api import OpenAIModelName
+from vet.imbue_core.agents.llm_apis.together_api import TogetherAIModelName
from vet.cli.config.loader import get_models_by_provider_from_config
from vet.cli.config.loader import get_user_defined_model_ids
from vet.cli.config.schema import ModelsConfig
diff --git a/vet/conftest.py b/vet/conftest.py
@@ -3,8 +3,8 @@ from typing import Generator
import pytest
-from imbue_core.async_monkey_patches_test import explode_on_error # noqa: F401
-from imbue_core.test_repo_utils import make_simple_test_git_repo
+from vet.imbue_core.async_monkey_patches_test import explode_on_error # noqa: F401
+from vet.imbue_core.test_repo_utils import make_simple_test_git_repo
simple_test_git_repo = pytest.fixture(make_simple_test_git_repo)
diff --git a/vet/formatters.py b/vet/formatters.py
@@ -2,7 +2,7 @@ from __future__ import annotations
from pydantic import BaseModel
-from imbue_core.data_types import IdentifiedVerifyIssue
+from vet.imbue_core.data_types import IdentifiedVerifyIssue
OUTPUT_FORMATS = ["text", "json"]
diff --git a/vet/git.py b/vet/git.py
@@ -9,7 +9,7 @@ from typing import Sequence
import anyio
from loguru import logger
-from imbue_core.async_monkey_patches import log_exception
+from vet.imbue_core.async_monkey_patches import log_exception
from vet.errors import RunCommandError
# Flexible path type alias
diff --git a/imbue_core/imbue_core/__init__.py b/vet/imbue_core/__init__.py
diff --git a/imbue_core/imbue_core/agents/__init__.py b/vet/imbue_core/agents/__init__.py
diff --git a/imbue_core/imbue_core/agents/agent_api/__init__.py b/vet/imbue_core/agents/agent_api/__init__.py
diff --git a/vet/imbue_core/agents/agent_api/api.py b/vet/imbue_core/agents/agent_api/api.py
@@ -0,0 +1,59 @@
+from __future__ import annotations
+
+from contextlib import contextmanager
+from functools import singledispatch
+from pathlib import Path
+from typing import Any
+from typing import ContextManager
+from typing import Iterator
+
+from vet.imbue_core.agents.agent_api.claude.client import ClaudeCodeClient
+from vet.imbue_core.agents.agent_api.claude.data_types import ClaudeCodeOptions
+from vet.imbue_core.agents.agent_api.client import AgentClient
+from vet.imbue_core.agents.agent_api.client import AgentOptionsT
+from vet.imbue_core.agents.agent_api.client import CachedAgentClient
+from vet.imbue_core.agents.agent_api.codex.client import CodexClient
+from vet.imbue_core.agents.agent_api.codex.data_types import CodexOptions
+from vet.imbue_core.agents.agent_api.data_types import AgentOptions
+
+
+@singledispatch
+def _build_client_from_options(
+ options: AgentOptions,
+) -> ContextManager[AgentClient[Any]]:
+ """Return a context manager that builds an AgentClient for the given options."""
+ raise ValueError(f"Unsupported agent options type: {type(options).__name__}")
+
+
+@_build_client_from_options.register
+def _(options: ClaudeCodeOptions) -> ContextManager[AgentClient[ClaudeCodeOptions]]:
+ return ClaudeCodeClient.build(options)
+
+
+@_build_client_from_options.register
+def _(options: CodexOptions) -> ContextManager[AgentClient[CodexOptions]]:
+ return CodexClient.build(options)
+
+
+@contextmanager
+def get_agent_client(
+ *,
+ options: AgentOptionsT,
+ cache_path: Path | None = None,
+) -> Iterator[AgentClient[AgentOptionsT]]:
+ """Build and manage the lifecycle of an AgentClient based on the provided options.
+
+ Args:
+ options: AgentOptions instance describing which agent to run.
+ cache_path: Optional path to use for caching agent interactions.
+
+ Yields:
+ An AgentClient (or CachedAgentClient) bound to the selected agent implementation.
+ """
+
+ with _build_client_from_options(options) as client:
+ if cache_path is None:
+ yield client
+ return
+
+ yield CachedAgentClient(client, cache_path)
diff --git a/vet/imbue_core/agents/agent_api/cache_utils.py b/vet/imbue_core/agents/agent_api/cache_utils.py
@@ -0,0 +1,36 @@
+import hashlib
+from pathlib import Path
+
+from vet.imbue_core.agents.agent_api.data_types import AgentOptions
+from vet.imbue_core.agents.agent_api.interaction import AgentInteractionRecord
+from vet.imbue_core.caching import get_cache
+
+
+def _create_cache_key(prompt: str, options: AgentOptions) -> str:
+ """Create a cache key for the given prompt and options."""
+ return hashlib.md5(f"{prompt} | {options.model_dump_json() if options else ''}".encode()).hexdigest()
+
+
+def check_cache(cache_path: Path, prompt: str, options: AgentOptions) -> AgentInteractionRecord | None:
+ """Check the cache for the given prompt and options."""
+ cache_key = _create_cache_key(prompt, options)
+ cache = get_cache(cache_path)
+
+ with cache:
+ value = cache.get(cache_key)
+
+ if value is None:
+ return None
+ assert isinstance(value, str), f"Got value of type {type(value)} from cache, expected str"
+ return AgentInteractionRecord.model_validate_json(value)
+
+
+def update_cache(
+ agent_interaction: AgentInteractionRecord,
+ cache_dir: Path,
+) -> None:
+ """Save an agent interaction record to the cache."""
+ cache = get_cache(cache_dir)
+ cache_key = _create_cache_key(agent_interaction.prompt, agent_interaction.options)
+ with cache:
+ cache.set(cache_key, agent_interaction.model_dump_json())
diff --git a/imbue_core/imbue_core/agents/agent_api/claude/__init__.py b/vet/imbue_core/agents/agent_api/claude/__init__.py
diff --git a/vet/imbue_core/agents/agent_api/claude/client.py b/vet/imbue_core/agents/agent_api/claude/client.py
@@ -0,0 +1,191 @@
+import json
+import shutil
+import tempfile
+from contextlib import contextmanager
+from pathlib import Path
+from typing import Generator
+from typing import Iterator
+from typing import Self
+
+from loguru import logger
+
+from vet.imbue_core.agents.agent_api.claude.data_types import ClaudeCodeOptions
+from vet.imbue_core.agents.agent_api.claude.message_parser import parse_claude_message
+from vet.imbue_core.agents.agent_api.client import RealAgentClient
+from vet.imbue_core.agents.agent_api.data_types import AgentMessage
+from vet.imbue_core.agents.agent_api.data_types import AgentResultMessage
+from vet.imbue_core.agents.agent_api.errors import AgentCLINotFoundError
+from vet.imbue_core.agents.agent_api.transport import AgentSubprocessCLITransport
+from vet.imbue_core.agents.agent_api.transport import AgentSubprocessCLITransportOptions
+from vet.imbue_core.agents.agent_api.transport import AgentTransport
+
+
+class ClaudeCodeClient(RealAgentClient[ClaudeCodeOptions]):
+ """Claude Code client implementation.
+
+ Most callers should obtain an instance through `get_agent_client(options=ClaudeCodeOptions(...))`,
+ which takes care of building and tearing down the underlying CLI transport.
+
+ Example:
+ ```python
+ with get_agent_client(options=ClaudeCodeOptions()) as client:
+ for message in client.process_query(prompt="Hello"):
+ print(message)
+ ```
+ """
+
+ def __init__(self, options: ClaudeCodeOptions, transport: AgentTransport) -> None:
+ super().__init__(options)
+ self._transport = transport
+
+ @classmethod
+ @contextmanager
+ def build(cls, options: ClaudeCodeOptions) -> Generator[Self, None, None]:
+ cmd = cls._build_cli_cmd(options)
+ with AgentSubprocessCLITransport.build(
+ AgentSubprocessCLITransportOptions(
+ cmd=cmd,
+ cwd=options.cwd,
+ extra_env_vars={"CLAUDE_CODE_ENTRYPOINT": "sdk-py"},
+ )
+ ) as transport:
+ yield cls(options=options, transport=transport)
+
+ def process_query(self, prompt: str) -> Iterator[AgentMessage]:
+ logger.trace(
+ "{client_name}: calling agent with prompt={prompt}",
+ client_name=type(self).__name__,
+ prompt=prompt,
+ )
+ # Claude code expects "User message" objects as inputs
+ self._transport.send_request(
+ [
+ {
+ "type": "user",
+ "message": {
+ "role": "user",
+ "content": [{"type": "text", "text": prompt}],
+ },
+ }
+ ],
+ self._options,
+ )
+
+ for data in self._transport.receive_messages():
+ logger.trace(
+ "{client_name}: received raw JSON message={data}",
+ client_name=type(self).__name__,
+ data=data,
+ )
+
+ message = parse_claude_message(data)
+ if message:
+ yield message
+
+ if isinstance(message, AgentResultMessage):
+ break
+
+ logger.trace(
+ "{client_name}: finished calling agent with prompt={prompt}",
+ client_name=type(self).__name__,
+ prompt=prompt,
+ )
+
+ @staticmethod
+ def _find_cli() -> str:
+ """Find Claude Code CLI binary."""
+ cli = shutil.which("claude")
+ if cli:
+ return cli
+
+ locations = [
+ # TODO: Document what these do. Does the path to claude inside the container need to be here?
+ Path("/imbue_addons/bin/claude"),
+ Path.home() / ".npm-global/bin/claude",
+ Path("/usr/local/bin/claude"),
+ Path.home() / ".local/bin/claude",
+ Path.home() / "node_modules/.bin/claude",
+ Path.home() / ".yarn/bin/claude",
+ ]
+
+ for path in locations:
+ if path.exists() and path.is_file():
+ return str(path)
+
+ node_installed = shutil.which("node") is not None
+
+ if not node_installed:
+ raise AgentCLINotFoundError(
+ "\n".join(
+ [
+ "Claude Code requires Node.js, which is not installed.",
+ "Install Node.js from: https://nodejs.org/",
+ "\nAfter installing Node.js, install Claude Code:",
+ " npm install -g @anthropic-ai/claude-code",
+ ]
+ )
+ )
+
+ raise AgentCLINotFoundError(
+ "\n".join(
+ [
+ "Claude Code not found. Install with:",
+ " npm install -g @anthropic-ai/claude-code",
+ "\nIf already installed locally, try:",
+ ' export PATH="$HOME/node_modules/.bin:$PATH"',
+ ]
+ )
+ )
+
+ @classmethod
+ def _build_cli_cmd(cls, options: ClaudeCodeOptions) -> list[str]:
+ """Build CLI command with arguments."""
+ if options.is_cached:
+ # in this case, the cmd should never be used
+ cmd = ["CACHED_CLAUDE_CODE_EXEC_PLACEHOLDER"]
+ return cmd
+ cli_path = str(options.cli_path) if options.cli_path is not None else cls._find_cli()
+ cmd = [
+ cli_path,
+ "--output-format",
+ "stream-json",
+ "--input-format",
+ "stream-json",
+ "--verbose",
+ ]
+ cmd.extend(cls._build_cli_args(options))
+ return cmd
+
+ @staticmethod
+ def _build_cli_args(options: ClaudeCodeOptions) -> list[str]:
+ args = []
+ if options.system_prompt:
+ args.extend(["--system-prompt", options.system_prompt])
+
+ if options.append_system_prompt:
+ args.extend(["--append-system-prompt", options.append_system_prompt])
+
+ if options.model:
+ args.extend(["--model", options.model])
+
+ if options.permission_prompt_tool_name:
+ args.extend(["--permission-prompt-tool", options.permission_prompt_tool_name])
+
+ if options.permission_mode:
+ args.extend(["--permission-mode", options.permission_mode])
+
+ if options.continue_conversation:
+ args.append("--continue")
+
+ if options.resume:
+ args.extend(["--resume", options.resume])
+
+ if options.mcp_servers:
+ mcp_config_file = tempfile.NamedTemporaryFile(delete=False, suffix=".json")
+ mcp_config_file.write(
+ json.dumps({"mcpServers": {k: v.model_dump() for k, v in options.mcp_servers.items()}}).encode("utf-8")
+ )
+ args.extend(["--mcp-config", mcp_config_file.name])
+
+ args.append("--print")
+ return args
diff --git a/vet/imbue_core/agents/agent_api/claude/data_types.py b/vet/imbue_core/agents/agent_api/claude/data_types.py
@@ -0,0 +1,79 @@
+from pathlib import Path
+from typing import Literal
+
+from pydantic import Field
+
+from vet.imbue_core.agents.agent_api.data_types import AgentOptions
+from vet.imbue_core.agents.agent_api.data_types import AgentToolName
+from vet.imbue_core.pydantic_serialization import SerializableModel
+
+ClaudePermissionMode = Literal["plan", "default", "acceptEdits", "bypassPermissions"]
+
+
+class ClaudeMcpStdioServerConfig(SerializableModel):
+ """MCP stdio server configuration."""
+
+ type: Literal["stdio"] = "stdio"
+ command: str
+ args: list[str] = Field(default_factory=list)
+ env: dict[str, str] = Field(default_factory=dict)
+
+
+class ClaudeMcpHttpServerConfig(SerializableModel):
+ """MCP HTTP server configuration."""
+
+ type: Literal["http"] = "http"
+ url: str
+ headers: dict[str, str] | None = None
+
+
+ClaudeMcpServerConfig = ClaudeMcpStdioServerConfig | ClaudeMcpHttpServerConfig
+
+
+class ClaudeCodeOptions(AgentOptions):
+ """Query options for Claude SDK."""
+
+ object_type: Literal["ClaudeCodeOptions"] = "ClaudeCodeOptions"
+
+ allowed_tools: list[str] = Field(default_factory=list)
+ max_thinking_tokens: int = 8000
+ system_prompt: str | None = None
+ append_system_prompt: str | None = None
+ mcp_tools: list[str] = Field(default_factory=list)
+ mcp_servers: dict[str, ClaudeMcpServerConfig] = Field(default_factory=dict)
+ permission_mode: ClaudePermissionMode | None = None
+ continue_conversation: bool = False
+ resume: str | None = None
+ max_turns: int | None = None
+ disallowed_tools: list[str] = Field(default_factory=list)
+ model: str | None = None
+ permission_prompt_tool_name: str | None = None
+ # Optional override for the Claude CLI path
+ cli_path: Path | None = None
+ is_cached: bool = False
+
+
+CLAUDE_TOOLS = (
+ AgentToolName.READ,
+ AgentToolName.WRITE,
+ AgentToolName.EDIT,
+ AgentToolName.MULTI_EDIT,
+ AgentToolName.GLOB,
+ AgentToolName.NOTEBOOK_READ,
+ AgentToolName.NOTEBOOK_EDIT,
+ AgentToolName.LS,
+ AgentToolName.GREP,
+ AgentToolName.BASH,
+ AgentToolName.BASH_OUTPUT,
+ AgentToolName.KILL_SHELL,
+ AgentToolName.WEB_SEARCH,
+ AgentToolName.WEB_FETCH,
+ AgentToolName.TASK,
+ AgentToolName.TODO_READ,
+ AgentToolName.TODO_WRITE,
+ AgentToolName.SLASH_COMMAND,
+ AgentToolName.EXIT_PLAN_MODE,
+ AgentToolName.MCP_TOOL,
+ AgentToolName.LIST_MCP_RESOURCES,
+ AgentToolName.READ_MCP_RESOURCE,
+)
diff --git a/vet/imbue_core/agents/agent_api/claude/message_parser.py b/vet/imbue_core/agents/agent_api/claude/message_parser.py
@@ -0,0 +1,117 @@
+from typing import Any
+from typing import assert_never
+
+from vet.imbue_core.agents.agent_api.data_types import AgentAssistantMessage
+from vet.imbue_core.agents.agent_api.data_types import AgentContentBlock
+from vet.imbue_core.agents.agent_api.data_types import AgentMessage
+from vet.imbue_core.agents.agent_api.data_types import AgentResultMessage
+from vet.imbue_core.agents.agent_api.data_types import AgentSystemEventType
+from vet.imbue_core.agents.agent_api.data_types import AgentSystemMessage
+from vet.imbue_core.agents.agent_api.data_types import AgentTextBlock
+from vet.imbue_core.agents.agent_api.data_types import AgentThinkingBlock
+from vet.imbue_core.agents.agent_api.data_types import AgentToolResultBlock
+from vet.imbue_core.agents.agent_api.data_types import AgentToolUseBlock
+from vet.imbue_core.agents.agent_api.data_types import AgentUsage
+from vet.imbue_core.agents.agent_api.data_types import AgentUserMessage
+
+
+def parse_claude_message(data: dict[str, Any]) -> AgentMessage | None:
+ """Parse message from CLI output using unified types.
+
+ Reference:
+ https://github.com/anthropics/claude-agent-sdk-python/blob/main/src/claude_agent_sdk/_internal/message_parser.py
+ https://docs.claude.com/en/api/agent-sdk/typescript#sdkmessage
+ https://docs.claude.com/en/api/agent-sdk/python#message-types
+ """
+
+ match data["type"]:
+ case "user":
+ return AgentUserMessage(content=parse_claude_content_blocks(data), original_message=data)
+
+ case "assistant":
+ return AgentAssistantMessage(content=parse_claude_content_blocks(data), original_message=data)
+
+ case "system":
+ # Normalize system event types
+ event_type = parse_claude_system_event_type(data.get("subtype", ""))
+ return AgentSystemMessage(
+ event_type=event_type,
+ session_id=data.get("session_id"),
+ error=data.get("error"),
+ original_message=data,
+ )
+
+ case "result":
+ # Build normalized usage
+ usage = None
+ raw_usage = data.get("usage")
+ if raw_usage or data.get("total_cost_usd"):
+ usage = AgentUsage(
+ input_tokens=raw_usage.get("input_tokens") if raw_usage else None,
+ output_tokens=raw_usage.get("output_tokens") if raw_usage else None,
+ cached_tokens=(raw_usage.get("cache_read_input_tokens") if raw_usage else None),
+ total_tokens=(
+ raw_usage.get("input_tokens", 0) + raw_usage.get("output_tokens", 0) if raw_usage else None
+ ),
+ total_cost_usd=data.get("total_cost_usd"),
+ )
+
+ return AgentResultMessage(
+ session_id=data["session_id"],
+ is_error=data["is_error"],
+ duration_ms=data.get("duration_ms"),
+ api_duration_ms=data.get("duration_api_ms"),
+ num_turns=data.get("num_turns"),
+ usage=usage,
+ result=data.get("result"),
+ error=data.get("error") if data["is_error"] else None,
+ original_message=data,
+ )
+
+ case _ as unreachable:
+ assert_never(unreachable)
+
+
+def parse_claude_system_event_type(subtype: str) -> AgentSystemEventType:
+ """Parse Claude system event subtype to unified event type."""
+ subtype_lower = subtype.lower()
+
+ # TODO add other system event types as we find them
+ # basically the documentattion doesn't mention any other system event types
+ # other than init AFAIKT
+ if "init" in subtype_lower:
+ return AgentSystemEventType.SESSION_STARTED
+ else:
+ return AgentSystemEventType.OTHER
+
+
+def parse_claude_content_blocks(data: dict[str, Any]) -> list[AgentContentBlock]:
+ return [parse_claude_content_block(block) for block in data["message"]["content"]]
+
+
+def parse_claude_content_block(block: dict[str, Any]) -> AgentContentBlock:
+ """Parse content block from CLI output using unified types."""
+
+ match block["type"]:
+ case "text":
+ return AgentTextBlock(text=block["text"])
+
+ case "thinking":
+ # Claude Code thinking blocks
+ return AgentThinkingBlock(
+ content=block.get("thinking", ""),
+ thinking_tokens=block.get("thinking_tokens"),
+ )
+
+ case "tool_use":
+ return AgentToolUseBlock(id=block["id"], name=block["name"], input=block["input"])
+
+ case "tool_result":
+ return AgentToolResultBlock(
+ tool_use_id=block["tool_use_id"],
+ content=block.get("content"),
+ is_error=block.get("is_error"),
+ )
+
+ case _ as unreachable:
+ assert_never(unreachable)
diff --git a/vet/imbue_core/agents/agent_api/client.py b/vet/imbue_core/agents/agent_api/client.py
@@ -0,0 +1,89 @@
+import abc
+from pathlib import Path
+from typing import Generic
+from typing import Iterator
+from typing import TypeVar
+
+from vet.imbue_core.agents.agent_api.cache_utils import check_cache
+from vet.imbue_core.agents.agent_api.cache_utils import update_cache
+from vet.imbue_core.agents.agent_api.data_types import AgentMessage
+from vet.imbue_core.agents.agent_api.data_types import AgentOptions
+from vet.imbue_core.agents.agent_api.interaction import AgentInteraction
+from vet.imbue_core.agents.agent_api.interaction import AgentInteractionRecord
+
+AgentOptionsT = TypeVar("AgentOptionsT", bound=AgentOptions)
+
+
+class AgentClient(abc.ABC, Generic[AgentOptionsT]):
+ """Base code agent client interface.
+
+ This client defines the interface for launching and interacting with a coding agent (e.g., ClaudeCode, Codex, etc.)
+
+ Clients are usually created through `get_agent_client`, which selects the right concrete implementation
+ and manages any transports. Direct subclasses only need to implement `process_query`.
+ """
+
+ def __init__(self, options: AgentOptionsT) -> None:
+ self._options = options
+
+ @abc.abstractmethod
+ def process_query(self, prompt: str) -> Iterator[AgentMessage]:
+ """Call the underlying agent to process a query."""
+
+
+class RealAgentClient(AgentClient[AgentOptionsT]):
+ """Agent client that is not cached or dummy; it runs real commands."""
+
+ @staticmethod
+ @abc.abstractmethod
+ def _find_cli() -> str:
+ """Find the CLI binary for the agent."""
+
+ @classmethod
+ @abc.abstractmethod
+ def _build_cli_cmd(cls, options: AgentOptionsT) -> list[str]:
+ """Build the CLI command for the agent."""
+
+ @staticmethod
+ @abc.abstractmethod
+ def _build_cli_args(options: AgentOptionsT) -> list[str]:
+ """Build the CLI arguments for the agent."""
+
+
+class CachedAgentClient(AgentClient[AgentOptionsT]):
+ """Cached agent client implementation.
+
+ This client is a wrapper around an agent client that caches the agent responses.
+ """
+
+ def __init__(self, client: AgentClient[AgentOptionsT], cache_path: Path) -> None:
+ super().__init__(client._options)
+ self._client = client
+ self._cache_path = cache_path
+
+ def process_query(self, prompt: str) -> Iterator[AgentMessage]:
+ cache_path = self._cache_path
+ if cache_path is not None:
+ cache_record = check_cache(cache_path, prompt, self._client._options)
+ if cache_record is not None:
+ for message in cache_record.messages:
+ yield message
+ return
+
+ agent_interaction = AgentInteraction(prompt, self._client._options)
+ for message in self._client.process_query(prompt):
+ agent_interaction.put(message)
+ yield message
+
+ # NOTE we only cache full interactions given the 'process_query' method is called till
+ # the generator is exhausted.
+ # This means that if the generator is not exhausted, the cache will not be updated.
+ # If we do want a way to still cache interactions, even if we early exit the generator,
+ # then we could use a separate thread to get the agent response and cache it in the background.
+ agent_interaction_record = AgentInteractionRecord.from_agent_interaction(agent_interaction)
+ update_cache(agent_interaction_record, cache_path)
+
+ @property
+ def client(self) -> AgentClient[AgentOptionsT]:
+ """Get the underlying client."""
+ return self._client
diff --git a/imbue_core/imbue_core/agents/agent_api/codex/__init__.py b/vet/imbue_core/agents/agent_api/codex/__init__.py
diff --git a/vet/imbue_core/agents/agent_api/codex/client.py b/vet/imbue_core/agents/agent_api/codex/client.py
@@ -0,0 +1,166 @@
+import json
+import shutil
+from contextlib import contextmanager
+from pathlib import Path
+from typing import Generator
+from typing import Iterator
+from typing import Self
+
+from loguru import logger
+
+from vet.imbue_core.agents.agent_api.client import RealAgentClient
+from vet.imbue_core.agents.agent_api.codex.data_types import CodexOptions
+from vet.imbue_core.agents.agent_api.codex.message_parser import parse_codex_event
+from vet.imbue_core.agents.agent_api.data_types import AgentMessage
+from vet.imbue_core.agents.agent_api.data_types import AgentSystemMessage
+from vet.imbue_core.agents.agent_api.errors import AgentCLINotFoundError
+from vet.imbue_core.agents.agent_api.transport import AgentSubprocessCLITransport
+from vet.imbue_core.agents.agent_api.transport import AgentSubprocessCLITransportOptions
+
+
+class CodexClient(RealAgentClient[CodexOptions]):
+ """Codex CLI client implementation."""
+
+ def __init__(self, options: CodexOptions) -> None:
+ super().__init__(options=options)
+ self._session_id: str | None = options.resume_session_id
+
+ @classmethod
+ @contextmanager
+ def build(cls, options: CodexOptions) -> Generator[Self, None, None]:
+ yield cls(options=options)
+
+ def process_query(self, prompt: str) -> Iterator[AgentMessage]:
+ logger.trace(
+ "{client_name}: calling agent with prompt={prompt}",
+ client_name=type(self).__name__,
+ prompt=prompt,
+ )
+
+ # NOTE: (2025-11-20) Codex CLI does not support streaming inputs, and only supports using codex CLI via
+ # non-interactive mode, where each call is a new process.
+ # So here we just create a new transport for each call, and handle things like resuming the session as
+ # needed.
+ options = self._options
+ if self._session_id is not None and self._session_id != self._options.resume_session_id:
+ # Inject the current session id into the options before building the command
+ options = self._options.model_copy(update={"resume_session_id": self._session_id})
+ cmd = self._build_cli_cmd(options)
+ with AgentSubprocessCLITransport.build(
+ AgentSubprocessCLITransportOptions(cmd=[*cmd, prompt], cwd=options.cwd)
+ ) as transport:
+ transport.send_request([prompt], options)
+
+ thread_id: str | None = None
+ for data in transport.receive_messages():
+ logger.trace(
+ "{client_name}: received raw JSON message={data}",
+ client_name=type(self).__name__,
+ data=data,
+ )
+
+ message = parse_codex_event(data, thread_id)
+ if message:
+ if isinstance(message, AgentSystemMessage):
+ thread_id = message.session_id
+ # Store the new session id for subsequent calls to process_query on this client
+ self._session_id = message.session_id
+
+ yield message
+
+ logger.trace(
+ "{client_name}: finished calling agent with prompt={prompt}",
+ client_name=type(self).__name__,
+ prompt=prompt,
+ )
+
+ @staticmethod
+ def _find_cli() -> str:
+ """Find Codex CLI binary."""
+ cli = shutil.which("codex")
+ if cli:
+ return cli
+
+ locations = [
+ Path("/usr/local/bin/codex"),
+ Path.home() / ".local/bin/codex",
+ Path.home() / "node_modules/.bin/codex",
+ Path.home() / ".npm-global/bin/codex",
+ ]
+
+ for path in locations:
+ if path.exists() and path.is_file():
+ return str(path)
+
+ node_installed = shutil.which("node") is not None
+ npm_installed = shutil.which("npm") is not None
+
+ if not node_installed or not npm_installed:
+ raise AgentCLINotFoundError(
+ "\n".join(
+ [
+ "Codex CLI requires Node.js and npm, which may not be installed.",
+ "Install Node.js from: https://nodejs.org/",
+ "\nAfter installing Node.js, install Codex CLI:",
+ " npm install -g @openai/codex",
+ ]
+ )
+ )
+
+ raise AgentCLINotFoundError(
+ "\n".join(
+ [
+ "Codex CLI not found. Install with:",
+ " npm install -g @openai/codex",
+ "\nOr via Homebrew:",
+ " brew install codex",
+ "\nIf already installed locally, try:",
+ ' export PATH="$HOME/node_modules/.bin:$PATH"',
+ ]
+ )
+ )
+
+ @classmethod
+ def _build_cli_cmd(cls, options: CodexOptions) -> list[str]:
+ """Build CLI command with arguments."""
+ if options.is_cached:
+ # in this case, the cmd should never be used
+ cmd = ["CACHED_CODEX_EXEC_PLACEHOLDER"]
+ return cmd
+ cli_path = str(options.cli_path) if options.cli_path is not None else cls._find_cli()
+ cmd = [cli_path, "exec"]
+ cmd.extend(cls._build_cli_args(options))
+ return cmd
+
+ @staticmethod
+ def _build_cli_args(options: CodexOptions) -> list[str]:
+ args = []
+ # Permissions flags
+ if options.approval_mode:
+ args.extend(["-c", f"'approval_mode={options.approval_mode}'"])
+ if options.sandbox_mode:
+ args.extend(["--sandbox", options.sandbox_mode])
+ if options.approval_policy:
+ args.extend(["-c", f"'approval={options.approval_policy}'"])
+
+ # JSON streaming output
+ args.append("--json")
+
+ # Model selection
+ if options.model:
+ args.extend(["--model", options.model])
+
+ # Skip git repo check
+ if options.skip_git_repo_check:
+ args.append("--skip-git-repo-check")
+
+ # Output schema for structured output
+ if options.output_schema:
+ args.extend(["--output-schema", json.dumps(options.output_schema)])
+
+ # Session resumption
+ if options.resume_last:
+ args.extend(["resume", "--last"])
+ elif options.resume_session_id:
+ args.extend(["resume", options.resume_session_id])
+ return args
diff --git a/vet/imbue_core/agents/agent_api/codex/data_types.py b/vet/imbue_core/agents/agent_api/codex/data_types.py
@@ -0,0 +1,241 @@
+"""Data types for Codex agent integration."""
+
+from pathlib import Path
+from typing import Annotated
+from typing import Any
+from typing import Literal
+
+from pydantic import Field
+from pydantic import Tag
+
+from vet.imbue_core.agents.agent_api.data_types import AgentOptions
+from vet.imbue_core.agents.agent_api.data_types import AgentToolName
+from vet.imbue_core.pydantic_serialization import SerializableModel
+from vet.imbue_core.pydantic_serialization import build_discriminator
+
+# https://developers.openai.com/codex/cli/features#approval-modes
+CodexApprovalMode = Literal["auto", "read-only", "full-access"] | None
+
+# https://developers.openai.com/codex/cli/reference, --sandbox options
+CodexSandboxMode = Literal["read-only", "workspace-write", "danger-full-access"] | None
+
+# https://developers.openai.com/codex/cli/reference, --ask-for-approval options
+CodexApprovalPolicy = Literal["untrusted", "on-failure", "on-request", "never"] | None
+
+
+class CodexOptions(AgentOptions):
+ """Options for Codex CLI execution."""
+
+ object_type: Literal["CodexOptions"] = "CodexOptions"
+
+ approval_mode: CodexApprovalMode = None
+ sandbox_mode: CodexSandboxMode = None
+ approval_policy: CodexApprovalPolicy = None
+ model: str | None = None
+ system_prompt: str | None = None
+ image_paths: list[Path] = Field(default_factory=list)
+ skip_git_repo_check: bool = False
+ output_schema: dict[str, Any] | None = None
+ # Session management
+ resume_session_id: str | None = None
+ resume_last: bool = False
+ thread_id: str | None = None
+ # Optional override for the Codex CLI path
+ cli_path: Path | None = None
+ is_cached: bool = False
+
+
+# Codex item types
+# Ref: https://github.com/openai/codex/blob/main/sdk/typescript/src/items.ts
+# Ref: https://github.com/openai/codex/blob/main/codex-rs/exec/src/exec_events.rs
+
+
+# The status of a command execution.
+CommandExecutionStatus = Literal["in_progress", "completed", "failed"]
+
+
+class CodexCommandExecutionItem(SerializableModel):
+ type: Literal["command_execution"] = "command_execution"
+ id: str
+ command: str
+ aggregated_output: str
+ exit_code: int | None = None
+ status: CommandExecutionStatus
+
+
+# Indicates the type of the file change.
+PatchChangeKind = Literal["add", "delete", "update"]
+
+
+class CodexFileUpdateChange(SerializableModel):
+ path: str
+ kind: PatchChangeKind
+
+
+# The status of a file change.
+PatchApplyStatus = Literal["completed", "failed"]
+
+
+class CodexFileChangeItem(SerializableModel):
+ type: Literal["file_change"] = "file_change"
+ id: str
+ changes: list[CodexFileUpdateChange]
+ status: PatchApplyStatus
+
+
+# The status of an MCP tool call.
+McpToolCallStatus = Literal["in_progress", "completed", "failed"]
+
+
+class CodexMcpToolCallItem(SerializableModel):
+ type: Literal["mcp_tool_call"] = "mcp_tool_call"
+ id: str
+ server: str
+ tool: str
+ status: McpToolCallStatus
+
+
+class CodexAgentMessageItem(SerializableModel):
+ type: Literal["agent_message"] = "agent_message"
+ id: str
+ text: str
+
+
+class CodexReasoningItem(SerializableModel):
+ type: Literal["reasoning"] = "reasoning"
+ id: str
+ text: str
+
+
+class CodexWebSearchItem(SerializableModel):
+ type: Literal["web_search"] = "web_search"
+ id: str
+ query: str
+
+
+class CodexErrorItem(SerializableModel):
+ type: Literal["error"] = "error"
+ id: str
+ message: str
+
+
+class CodexTodoItem(SerializableModel):
+ text: str
+ completed: bool
+
+
+class CodexTodoListItem(SerializableModel):
+ type: Literal["todo_list"] = "todo_list"
+ id: str
+ items: list[CodexTodoItem]
+
+
+# Canonical union of thread items and their type-specific payloads.
+CodexThreadItemUnion = Annotated[
+ (
+ Annotated[CodexAgentMessageItem, Tag("agent_message")]
+ | Annotated[CodexReasoningItem, Tag("reasoning")]
+ | Annotated[CodexCommandExecutionItem, Tag("command_execution")]
+ | Annotated[CodexFileChangeItem, Tag("file_change")]
+ | Annotated[CodexMcpToolCallItem, Tag("mcp_tool_call")]
+ | Annotated[CodexWebSearchItem, Tag("web_search")]
+ | Annotated[CodexTodoListItem, Tag("todo_list")]
+ | Annotated[CodexErrorItem, Tag("error")]
+ ),
+ build_discriminator("type"),
+]
+
+
+# Codex (JSONL) event stream models
+# Ref:https://github.com/openai/codex/blob/main/sdk/typescript/src/events.ts
+
+
+class CodexThreadStartedEvent(SerializableModel):
+ type: Literal["thread.started"] = "thread.started"
+ thread_id: str
+
+
+class CodexTurnStartedEvent(SerializableModel):
+ type: Literal["turn.started"] = "turn.started"
+
+
+class CodexUsage(SerializableModel):
+ input_tokens: int
+ cached_input_tokens: int
+ output_tokens: int
+
+
+class CodexTurnCompletedEvent(SerializableModel):
+ type: Literal["turn.completed"] = "turn.completed"
+ usage: CodexUsage
+
+
+class CodexThreadError(SerializableModel):
+ message: str
+
+
+class CodexTurnFailedEvent(SerializableModel):
+ type: Literal["turn.failed"] = "turn.failed"
+ error: CodexThreadError
+
+
+class CodexItemStartedEvent(SerializableModel):
+ type: Literal["item.started"] = "item.started"
+ item: CodexThreadItemUnion
+
+
+class CodexItemUpdatedEvent(SerializableModel):
+ type: Literal["item.updated"] = "item.updated"
+ item: CodexThreadItemUnion
+
+
+class CodexItemCompletedEvent(SerializableModel):
+ type: Literal["item.completed"] = "item.completed"
+ item: CodexThreadItemUnion
+
+
+class CodexThreadErrorEvent(SerializableModel):
+ type: Literal["error"] = "error"
+ message: str
+
+
+CodexThreadEvent = Annotated[
+ (
+ Annotated[CodexThreadStartedEvent, Tag("thread.started")]
+ | Annotated[CodexTurnStartedEvent, Tag("turn.started")]
+ | Annotated[CodexTurnCompletedEvent, Tag("turn.completed")]
+ | Annotated[CodexTurnFailedEvent, Tag("turn.failed")]
+ | Annotated[CodexItemStartedEvent, Tag("item.started")]
+ | Annotated[CodexItemUpdatedEvent, Tag("item.updated")]
+ | Annotated[CodexItemCompletedEvent, Tag("item.completed")]
+ | Annotated[CodexThreadErrorEvent, Tag("error")]
+ ),
+ build_discriminator("type"),
+]
+
+# TODO: some of these might not actually be valid for codex!
+CODEX_TOOLS = (
+ AgentToolName.AGENT,
+ AgentToolName.BASH,
+ AgentToolName.EDIT,
+ AgentToolName.GLOB,
+ AgentToolName.GREP,
+ AgentToolName.LS,
+ AgentToolName.MULTI_EDIT,
+ AgentToolName.NOTEBOOK_EDIT,
+ AgentToolName.NOTEBOOK_READ,
+ AgentToolName.READ,
+ AgentToolName.TODO_READ,
+ AgentToolName.TODO_WRITE,
+ AgentToolName.WEB_FETCH,
+ AgentToolName.WEB_SEARCH,
+ AgentToolName.WRITE,
+ AgentToolName.COMPUTER,
+ AgentToolName.MEMORY,
+ AgentToolName.OTHER,
+ AgentToolName.CODE_EXECUTION,
+ AgentToolName.BASH_CODE_EXECUTION,
+ AgentToolName.TEXT_EDITOR_CODE_EXECUTION,
+ AgentToolName.COMMAND_EXECUTION,
+ AgentToolName.FILE_CHANGE,
+)
diff --git a/vet/imbue_core/agents/agent_api/codex/message_parser.py b/vet/imbue_core/agents/agent_api/codex/message_parser.py
@@ -0,0 +1,218 @@
+from typing import Any
+from typing import assert_never
+
+from pydantic import TypeAdapter
+
+from vet.imbue_core.agents.agent_api.codex.data_types import CodexAgentMessageItem
+from vet.imbue_core.agents.agent_api.codex.data_types import CodexCommandExecutionItem
+from vet.imbue_core.agents.agent_api.codex.data_types import CodexErrorItem
+from vet.imbue_core.agents.agent_api.codex.data_types import CodexFileChangeItem
+from vet.imbue_core.agents.agent_api.codex.data_types import CodexItemCompletedEvent
+from vet.imbue_core.agents.agent_api.codex.data_types import CodexItemStartedEvent
+from vet.imbue_core.agents.agent_api.codex.data_types import CodexItemUpdatedEvent
+from vet.imbue_core.agents.agent_api.codex.data_types import CodexMcpToolCallItem
+from vet.imbue_core.agents.agent_api.codex.data_types import CodexReasoningItem
+from vet.imbue_core.agents.agent_api.codex.data_types import CodexThreadErrorEvent
+from vet.imbue_core.agents.agent_api.codex.data_types import CodexThreadEvent
+from vet.imbue_core.agents.agent_api.codex.data_types import CodexThreadItemUnion
+from vet.imbue_core.agents.agent_api.codex.data_types import CodexThreadStartedEvent
+from vet.imbue_core.agents.agent_api.codex.data_types import CodexTodoListItem
+from vet.imbue_core.agents.agent_api.codex.data_types import CodexTurnCompletedEvent
+from vet.imbue_core.agents.agent_api.codex.data_types import CodexTurnFailedEvent
+from vet.imbue_core.agents.agent_api.codex.data_types import CodexTurnStartedEvent
+from vet.imbue_core.agents.agent_api.codex.data_types import CodexWebSearchItem
+from vet.imbue_core.agents.agent_api.data_types import AgentAssistantMessage
+from vet.imbue_core.agents.agent_api.data_types import AgentContentBlock
+from vet.imbue_core.agents.agent_api.data_types import AgentMessage
+from vet.imbue_core.agents.agent_api.data_types import AgentResultMessage
+from vet.imbue_core.agents.agent_api.data_types import AgentSystemEventType
+from vet.imbue_core.agents.agent_api.data_types import AgentSystemMessage
+from vet.imbue_core.agents.agent_api.data_types import AgentTextBlock
+from vet.imbue_core.agents.agent_api.data_types import AgentThinkingBlock
+from vet.imbue_core.agents.agent_api.data_types import AgentToolResultBlock
+from vet.imbue_core.agents.agent_api.data_types import AgentToolUseBlock
+from vet.imbue_core.agents.agent_api.data_types import AgentUsage
+
+
+def parse_codex_event(data: dict[str, Any], thread_id: str | None = None) -> AgentMessage | None:
+ """Parse Codex event into unified message.
+
+ Reference:
+ https://github.com/openai/codex/blob/main/docs/exec.md
+ https://github.com/openai/codex/blob/main/sdk/typescript/src/events.ts
+ """
+ codex_event = TypeAdapter(CodexThreadEvent).validate_python(data)
+ match codex_event:
+ case CodexThreadStartedEvent():
+ return AgentSystemMessage(
+ event_type=AgentSystemEventType.SESSION_STARTED,
+ session_id=codex_event.thread_id,
+ original_message=data,
+ )
+
+ case CodexTurnStartedEvent():
+ # Turn started within a thread. Nothing to do
+ return None
+
+ case CodexTurnCompletedEvent():
+ assert thread_id is not None, "thread_id is required for turn.completed event"
+ usage = AgentUsage(
+ input_tokens=codex_event.usage.input_tokens,
+ output_tokens=codex_event.usage.output_tokens,
+ cached_tokens=codex_event.usage.cached_input_tokens,
+ total_tokens=codex_event.usage.input_tokens + codex_event.usage.output_tokens,
+ )
+ return AgentResultMessage(
+ session_id=thread_id,
+ is_error=False,
+ usage=usage,
+ original_message=data,
+ )
+
+ case CodexTurnFailedEvent():
+ assert thread_id is not None, "thread_id is required for turn.failed event"
+ return AgentResultMessage(
+ session_id=thread_id,
+ is_error=True,
+ error=codex_event.error.message,
+ usage=None,
+ original_message=data,
+ )
+
+ case CodexItemStartedEvent():
+ content_blocks = parse_codex_item(codex_event.item)
+ return AgentAssistantMessage(content=content_blocks, original_message=data)
+
+ case CodexItemUpdatedEvent():
+ # Intermediate item, don't return anything
+ return None
+
+ case CodexItemCompletedEvent():
+ content_blocks = parse_codex_item(codex_event.item)
+ return AgentAssistantMessage(content=content_blocks, original_message=data)
+
+ case CodexThreadErrorEvent():
+ return AgentResultMessage(
+ session_id=thread_id or "",
+ is_error=True,
+ error=codex_event.message,
+ usage=None,
+ original_message=data,
+ )
+ case _ as unreachable:
+ assert_never(unreachable)
+
+
+def parse_codex_item(
+ item_data: dict[str, Any] | CodexThreadItemUnion,
+) -> list[AgentContentBlock]:
+ """Parse Codex item into unified content blocks.
+
+ Refs:
+ https://github.com/openai/codex/blob/main/sdk/typescript/src/items.ts
+ """
+ if isinstance(item_data, dict):
+ codex_item = TypeAdapter(CodexThreadItemUnion).validate_python(item_data)
+ else:
+ codex_item = item_data
+
+ match codex_item:
+ case CodexAgentMessageItem():
+ return [AgentTextBlock(text=codex_item.text)]
+
+ case CodexReasoningItem():
+ return [AgentThinkingBlock(content=codex_item.text)]
+
+ case CodexErrorItem():
+ return [AgentTextBlock(text=f"[Error: {codex_item.message}]")]
+
+ case CodexCommandExecutionItem():
+ if codex_item.status == "in_progress":
+ return [
+ AgentToolUseBlock(
+ id=codex_item.id,
+ name=codex_item.type,
+ input={"command": codex_item.command},
+ )
+ ]
+ return [
+ AgentToolResultBlock(
+ tool_use_id=codex_item.id,
+ content=codex_item.aggregated_output,
+ exit_code=codex_item.exit_code,
+ is_error=codex_item.exit_code != 0,
+ )
+ ]
+
+ case CodexFileChangeItem():
+ is_error = codex_item.status == "failed"
+ return [
+ AgentToolUseBlock(
+ id=codex_item.id,
+ name=codex_item.type,
+ input={"changes": [change.model_dump() for change in codex_item.changes]},
+ ),
+ AgentToolResultBlock(
+ tool_use_id=codex_item.id,
+ content=[change.model_dump() for change in codex_item.changes],
+ is_error=is_error,
+ exit_code=-1 if is_error else 0,
+ ),
+ ]
+
+ case CodexMcpToolCallItem():
+ if codex_item.status == "in_progress":
+ return [
+ AgentToolUseBlock(
+ id=codex_item.id,
+ name=codex_item.type,
+ input={"server": codex_item.server, "tool": codex_item.tool},
+ )
+ ]
+ # NOTE: currently (24-oct-2025) the MCP tool call item is not really well defined
+ # it does not have a result field or anything. So for now, we just return the server and tool as the content.
+ return [
+ AgentToolResultBlock(
+ tool_use_id=codex_item.id,
+ content=[{"server": codex_item.server, "tool": codex_item.tool}],
+ is_error=codex_item.status == "failed",
+ exit_code=-1 if codex_item.status == "failed" else 0,
+ )
+ ]
+
+ case CodexWebSearchItem():
+ # NOTE: currently (24-oct-2025) the web search item is not really well defined
+ # i.e. it only has a query field, and no other fields like results, progress, etc.
+ # so for now, so that each tool use has a matching result, we just return the query as the content.
+ return [
+ AgentToolUseBlock(
+ id=codex_item.id,
+ name=codex_item.type,
+ input={"query": codex_item.query},
+ ),
+ AgentToolResultBlock(
+ tool_use_id=codex_item.id,
+ content=codex_item.query,
+ # No error reported for web search
+ is_error=False,
+ exit_code=0,
+ ),
+ ]
+
+ case CodexTodoListItem():
+ return [
+ AgentToolUseBlock(
+ id=codex_item.id,
+ name=codex_item.type,
+ input={"todos": [item.model_dump() for item in codex_item.items]},
+ ),
+ AgentToolResultBlock(
+ tool_use_id=codex_item.id,
+ content=[item.model_dump() for item in codex_item.items],
+ is_error=False,
+ exit_code=0,
+ ),
+ ]
+
+ case _ as unreachable:
+ assert_never(unreachable)
diff --git a/vet/imbue_core/agents/agent_api/data_types.py b/vet/imbue_core/agents/agent_api/data_types.py
@@ -0,0 +1,252 @@
+import enum
+from pathlib import Path
+from typing import Annotated
+from typing import Any
+from typing import Literal
+
+from pydantic import Field
+from pydantic import Tag
+
+from vet.imbue_core.pydantic_serialization import SerializableModel
+from vet.imbue_core.pydantic_serialization import build_discriminator
+
+AgentPermissionMode = Literal["default", "acceptEdits", "bypassPermissions"]
+
+
+class AgentToolName(enum.StrEnum):
+ """Enumeration of all known coding agent tools across Claude Code and Codex.
+
+ This is a superset of tools available across different coding agents.
+ Not all tools are available in all agents.
+ """
+
+ # File operations
+ READ = "Read"
+ WRITE = "Write"
+ EDIT = "Edit"
+ MULTI_EDIT = "MultiEdit"
+ GLOB = "Glob"
+ NOTEBOOK_READ = "NotebookRead"
+ NOTEBOOK_EDIT = "NotebookEdit"
+ LS = "LS"
+
+ # Search operations
+ GREP = "Grep"
+
+ # Execution tools
+ BASH = "Bash"
+ BASH_OUTPUT = "BashOutput"
+ KILL_SHELL = "KillShell"
+
+ # Web operations
+ WEB_SEARCH = "WebSearch"
+ WEB_FETCH = "WebFetch"
+
+ # Agent orchestration
+ TASK = "Task"
+ TODO_READ = "TodoRead"
+ TODO_WRITE = "TodoWrite"
+ SLASH_COMMAND = "SlashCommand"
+ EXIT_PLAN_MODE = "exit_plan_mode"
+
+ # MCP tools
+ MCP_TOOL = "mcp_tool" # Generic MCP tool prefix
+ LIST_MCP_RESOURCES = "ListMcpResourcesTool"
+ READ_MCP_RESOURCE = "ReadMcpResourceTool"
+
+ # Code execution
+ CODE_EXECUTION = "code_execution"
+ BASH_CODE_EXECUTION = "bash_code_execution"
+ TEXT_EDITOR_CODE_EXECUTION = "text_editor_code_execution"
+
+ # Codex-specific operations
+ COMMAND_EXECUTION = "command_execution" # Codex's command execution
+ FILE_CHANGE = "file_change" # Codex's file change operation
+
+ # Other tools
+ AGENT = "Agent"
+ COMPUTER = "computer" # Computer use capability
+ MEMORY = "memory" # Memory storage
+ OTHER = "other" # Catch-all for unknown/custom tools
+
+
+# TODO: these are not, in the strict sense, read-only; perhaps we should have finer gradations
+READ_ONLY_TOOLS = (
+ AgentToolName.TASK,
+ AgentToolName.READ,
+ AgentToolName.GLOB,
+ AgentToolName.GREP,
+ AgentToolName.LS,
+ AgentToolName.BASH,
+ AgentToolName.NOTEBOOK_READ,
+ AgentToolName.TODO_READ,
+ AgentToolName.TODO_WRITE,
+ AgentToolName.WEB_FETCH,
+ AgentToolName.WEB_SEARCH,
+)
+
+
+# Content block types
+class AgentTextBlock(SerializableModel):
+ """Text content block.
+
+ Represents plain text output from the agent.
+ """
+
+ text: str
+
+
+class AgentThinkingBlock(SerializableModel):
+ """Agent's internal reasoning/thinking block.
+
+ Represents the agent's thought process or reasoning, which may be hidden
+ from the end user in some interfaces.
+ """
+
+ content: str
+ thinking_tokens: int | None = Field(default=None, description="Number of tokens used for thinking")
+
+
+class AgentToolUseBlock(SerializableModel):
+ """Tool invocation request.
+
+ Represents a request from the agent to use a specific tool.
+ """
+
+ id: str
+ name: AgentToolName | str # allow str for flexibility
+ input: dict[str, Any]
+
+
+class AgentToolResultBlock(SerializableModel):
+ """Tool execution result.
+
+ Represents the result of executing a tool, which is fed back to the agent.
+ """
+
+ tool_use_id: str
+ content: str | list[dict[str, Any]] | None = None
+ is_error: bool | None = None
+ exit_code: int | None = Field(default=None, description="Exit code for command executions")
+
+
+AgentContentBlock = AgentTextBlock | AgentThinkingBlock | AgentToolUseBlock | AgentToolResultBlock
+
+
+class AgentSystemEventType(enum.StrEnum):
+ """System event types
+
+ Super set of system event types across all agents.
+ """
+
+ SESSION_STARTED = "session_started"
+ SESSION_RESUMED = "session_resumed"
+ TURN_STARTED = "turn_started"
+ TURN_COMPLETED = "turn_completed"
+ TURN_FAILED = "turn_failed"
+ # For agent-specific events that don't fit into the above categories
+ OTHER = "other"
+
+
+# Message types (`type` field is required for serialization)
+class AgentUserMessage(SerializableModel):
+ """User message.
+
+ Represents input from the user to the agent.
+ """
+
+ object_type: Literal["AgentUserMessage"] = "AgentUserMessage"
+ content: str | list[AgentContentBlock]
+ original_message: dict[str, Any] | None = Field(default=None, description="Original agent-specific message data")
+
+
+class AgentAssistantMessage(SerializableModel):
+ """Assistant message with content blocks.
+
+ Represents output from the agent, which may include text, thinking,
+ tool uses, and tool results.
+ """
+
+ object_type: Literal["AgentAssistantMessage"] = "AgentAssistantMessage"
+ content: list[AgentContentBlock]
+ original_message: dict[str, Any] | None = Field(default=None, description="Original agent-specific message data")
+
+
+class AgentSystemMessage(SerializableModel):
+ """System message with normalized event data.
+
+ Represents lifecycle events from the agent session (e.g., turn started,
+ turn completed, session started).
+ """
+
+ object_type: Literal["AgentSystemMessage"] = "AgentSystemMessage"
+ event_type: AgentSystemEventType
+ session_id: str | None = Field(default=None, description="Session/thread identifier")
+ error: str | None = Field(default=None, description="Error message for failed events")
+ original_message: dict[str, Any] | None = Field(default=None, description="Original agent-specific message data")
+
+
+class AgentUsage(SerializableModel):
+ """Normalized usage tracking across agents.
+
+ Tracks token usage and costs in a unified format.
+ """
+
+ input_tokens: int | None = Field(default=None, description="Input/prompt tokens consumed")
+ output_tokens: int | None = Field(default=None, description="Output/completion tokens generated")
+ cached_tokens: int | None = Field(default=None, description="Cached input tokens reused")
+ total_tokens: int | None = Field(default=None, description="Total tokens (input + output)")
+ thinking_tokens: int | None = Field(default=None, description="Tokens used for extended thinking")
+ total_cost_usd: float | None = Field(default=None, description="Estimated cost in USD")
+
+
+class AgentResultMessage(SerializableModel):
+ """Result message with cost and usage information.
+
+ Represents the final result of an agent session, including timing,
+ usage statistics, and success/error status.
+ """
+
+ object_type: Literal["AgentResultMessage"] = "AgentResultMessage"
+ session_id: str
+ is_error: bool
+ duration_ms: int | None = Field(default=None, description="Total duration in milliseconds")
+ api_duration_ms: int | None = Field(default=None, description="API call duration in milliseconds")
+ num_turns: int | None = Field(default=None, description="Number of conversation turns")
+ usage: AgentUsage | None = Field(default=None, description="Token usage and cost information")
+ result: str | None = Field(default=None, description="Final result or output from the agent")
+ error: str | None = Field(default=None, description="Error message if is_error=True")
+ original_message: dict[str, Any] | None = Field(default=None, description="Original agent-specific message data")
+
+
+AgentMessage = AgentUserMessage | AgentAssistantMessage | AgentSystemMessage | AgentResultMessage
+AgentMessageUnion = Annotated[
+ Annotated[AgentUserMessage, Tag("AgentUserMessage")]
+ | Annotated[AgentAssistantMessage, Tag("AgentAssistantMessage")]
+ | Annotated[AgentSystemMessage, Tag("AgentSystemMessage")]
+ | Annotated[AgentResultMessage, Tag("AgentResultMessage")],
+ build_discriminator(),
+]
+
+
+class ToolUseRecord(SerializableModel):
+ """A record of a tool use."""
+
+ request_message: AgentToolUseBlock
+ result_message: AgentToolResultBlock
+
+ @property
+ def tool_name(self) -> str:
+ """The name of the tool used."""
+ return self.request_message.name
+
+ @property
+ def tool_input(self) -> dict[str, Any]:
+ """The input to the tool."""
+ return self.request_message.input
+
+
+class AgentOptions(SerializableModel):
+ """Parent class for all agent options."""
+
+ cwd: str | Path | None = None
diff --git a/imbue_core/imbue_core/agents/agent_api/errors.py b/vet/imbue_core/agents/agent_api/errors.py
diff --git a/vet/imbue_core/agents/agent_api/interaction.py b/vet/imbue_core/agents/agent_api/interaction.py
@@ -0,0 +1,96 @@
+from typing import Sequence
+
+from vet.imbue_core.agents.agent_api.data_types import AgentAssistantMessage
+from vet.imbue_core.agents.agent_api.data_types import AgentMessage
+from vet.imbue_core.agents.agent_api.data_types import AgentOptions
+from vet.imbue_core.agents.agent_api.data_types import AgentToolResultBlock
+from vet.imbue_core.agents.agent_api.data_types import AgentToolUseBlock
+from vet.imbue_core.agents.agent_api.data_types import AgentUserMessage
+from vet.imbue_core.agents.agent_api.data_types import ToolUseRecord
+from vet.imbue_core.pydantic_serialization import SerializableModel
+
+
+class AgentInteraction:
+ """A class for tracking an ongoing interaction with an agent.
+
+ Note that this class is not thread-safe.
+ """
+
+ def __init__(self, prompt: str, options: AgentOptions) -> None:
+ self.prompt = prompt
+ self.options = options
+ self.messages: list[AgentMessage] = []
+ self.tool_use_records: list[ToolUseRecord] = []
+ self._unresolved_tool_use_requests: list[AgentToolUseBlock] = []
+
+ def put(self, message: AgentMessage) -> None:
+ self.messages.append(message)
+
+ if isinstance(message, AgentAssistantMessage):
+ for assistant_content_block in message.content:
+ if isinstance(assistant_content_block, AgentToolUseBlock):
+ self._unresolved_tool_use_requests.append(assistant_content_block)
+ elif isinstance(message, AgentUserMessage) and isinstance(message.content, list):
+ for content_block in message.content:
+ if isinstance(content_block, AgentToolResultBlock):
+ remaining_unresolved_requests = []
+ for request in self._unresolved_tool_use_requests:
+ if request.id == content_block.tool_use_id:
+ self.tool_use_records.append(
+ ToolUseRecord(
+ request_message=request,
+ result_message=content_block,
+ )
+ )
+ else:
+ remaining_unresolved_requests.append(request)
+ self._unresolved_tool_use_requests = remaining_unresolved_requests
+
+ def find_tool_use_record_by_command(self, command: str, by_most_recent: bool = True) -> ToolUseRecord | None:
+ """Look for tool use request and result messages by the tool command.
+
+ If by_most_recent is True, the records are searched in reverse order (most recent first).
+ """
+ return _find_tool_use_record_by_command(self.tool_use_records, command, by_most_recent)
+
+
+class AgentInteractionRecord(SerializableModel):
+ """A serializable record of a completed agent interaction.
+
+ This is meant to be used for storing a completed log in a database or cache.
+ """
+
+ prompt: str
+ options: AgentOptions
+ messages: tuple[AgentMessage, ...]
+ tool_use_records: tuple[ToolUseRecord, ...]
+
+ @classmethod
+ def from_agent_interaction(cls, agent_interaction: AgentInteraction) -> "AgentInteractionRecord":
+ return cls(
+ prompt=agent_interaction.prompt,
+ options=agent_interaction.options,
+ messages=tuple(agent_interaction.messages),
+ tool_use_records=tuple(agent_interaction.tool_use_records),
+ )
+
+ def find_tool_use_record_by_command(self, command: str, by_most_recent: bool = True) -> ToolUseRecord | None:
+ """Look for tool use request and result messages by the tool command.
+
+ If by_most_recent is True, the records are searched in reverse order (most recent first).
+ """
+ return _find_tool_use_record_by_command(self.tool_use_records, command, by_most_recent)
+
+
+def _find_tool_use_record_by_command(
+ tool_use_records: Sequence[ToolUseRecord], command: str, reverse: bool = True
+) -> ToolUseRecord | None:
+ """Look for tool use request and result messages by the tool command.
+
+ If reverse is True, the records are searched in reverse order (most recent first).
+ """
+ for record in reversed(tool_use_records) if reverse else tool_use_records:
+ tool_input = record.tool_input
+ if "command" in tool_input and tool_input["command"] == command:
+ return record
+ return None
diff --git a/vet/imbue_core/agents/agent_api/transport.py b/vet/imbue_core/agents/agent_api/transport.py
@@ -0,0 +1,176 @@
+import json
+import os
+import subprocess
+import threading
+from abc import ABC
+from abc import abstractmethod
+from contextlib import contextmanager
+from pathlib import Path
+from subprocess import PIPE
+from typing import Any
+from typing import ContextManager
+from typing import Generator
+from typing import Generic
+from typing import Iterable
+from typing import Iterator
+from typing import Self
+from typing import Sequence
+from typing import TypeVar
+
+from vet.imbue_core.agents.agent_api.data_types import AgentOptions
+from vet.imbue_core.agents.agent_api.errors import AgentCLIConnectionError
+from vet.imbue_core.agents.agent_api.errors import (
+ AgentCLIJSONDecodeError as SDKJSONDecodeError,
+)
+from vet.imbue_core.agents.agent_api.errors import AgentCLINotFoundError
+from vet.imbue_core.agents.agent_api.errors import AgentProcessError
+from vet.imbue_core.pydantic_serialization import SerializableModel
+
+TransportOptionsT = TypeVar("TransportOptionsT", bound=SerializableModel)
+
+
+class AgentTransport(ABC, Generic[TransportOptionsT]):
+ """Abstract transport for Agent communication."""
+
+ @classmethod
+ @abstractmethod
+ def build(cls, options: TransportOptionsT) -> ContextManager[Self]:
+ """Build a transport from options.
+
+ This is the main entry point for building a transport and managing its lifecycle.
+ """
+
+ @abstractmethod
+ def send_request(self, messages: list[Any], agent_options: AgentOptions) -> None:
+ """Send request to underlying agent via transport."""
+
+ @abstractmethod
+ def receive_messages(self) -> Iterator[dict[str, Any]]:
+ """Receive messages from underlying agent via transport."""
+
+ @abstractmethod
+ def is_connected(self) -> bool:
+ """Check if transport is connected."""
+
+
+class AgentSubprocessCLITransportOptions(SerializableModel):
+ """Options for AgentSubprocessCLITransport."""
+
+ cmd: Sequence[str]
+ cwd: str | Path | None = None
+ extra_env_vars: dict[str, str] | None = None
+
+
+class AgentSubprocessCLITransport(AgentTransport[AgentSubprocessCLITransportOptions]):
+ """Subprocess transport using Coding Agent via a CLI."""
+
+ def __init__(
+ self,
+ popen: subprocess.Popen[str],
+ ) -> None:
+ self._process = popen
+ self._stdin_stream = popen.stdin
+ self._stdout_stream = popen.stdout
+ self._stderr_stream = popen.stderr
+
+ @classmethod
+ @contextmanager
+ def build(cls, options: AgentSubprocessCLITransportOptions) -> Generator[Self, None, None]:
+ extra_env_vars = options.extra_env_vars or {}
+ try:
+ popen = subprocess.Popen(
+ options.cmd,
+ stdin=PIPE,
+ stdout=PIPE,
+ stderr=PIPE,
+ cwd=options.cwd,
+ env={**os.environ, **extra_env_vars},
+ # ensure output is line buffered
+ bufsize=1,
+ text=True,
+ encoding="utf-8",
+ )
+ except FileNotFoundError as e:
+ raise AgentCLINotFoundError(f"Agent CLI not found for: cmd={options.cmd}") from e
+ except Exception as e:
+ raise AgentCLIConnectionError(f"Failed to start Agent CLI via cmd={options.cmd}: {e}") from e
+
+ try:
+ yield cls(popen)
+ finally:
+ # Make sure to terminate the process if it is still running, and clean up the streams
+ if popen.poll() is None:
+ try:
+ popen.terminate()
+ popen.wait(timeout=5.0)
+ except subprocess.TimeoutExpired:
+ popen.kill()
+ popen.wait(timeout=5.0)
+ popen.stdout and popen.stdout.close()
+ popen.stderr and popen.stderr.close()
+ popen.stdin and popen.stdin.close()
+
+ def send_request(self, messages: Iterable[dict[str, Any] | str], agent_options: AgentOptions) -> None:
+ process = self._process
+ stdin_stream = self._stdin_stream
+ if not process or not stdin_stream:
+ raise AgentCLIConnectionError("Not connected")
+
+ for message in messages:
+ stdin_stream.write(json.dumps(message) + "\n")
+ stdin_stream.flush()
+
+ def _read_stderr(self, output_buffer: list[str]) -> None:
+ """Read stderr in background."""
+ stderr_stream = self._stderr_stream
+ if stderr_stream:
+ try:
+ for line in stderr_stream:
+ output_buffer.append(line.strip())
+ except subprocess.SubprocessError:
+ pass
+
+ def receive_messages(self) -> Iterator[dict[str, Any]]:
+ process = self._process
+ stdout_stream = self._stdout_stream
+ if not process or not stdout_stream:
+ raise AgentCLIConnectionError("Not connected")
+
+ stderr_lines: list[str] = []
+ stderr_read_thread = threading.Thread(target=self._read_stderr, args=(stderr_lines,))
+ stderr_read_thread.start()
+
+ try:
+ for line in stdout_stream:
+ line_str = line.strip()
+ if not line_str:
+ continue
+
+ try:
+ data = json.loads(line_str)
+ try:
+ yield data
+ except GeneratorExit:
+ # Handle generator cleanup gracefully
+ return
+ except json.JSONDecodeError as e:
+ if line_str.startswith("{") or line_str.startswith("["):
+ raise SDKJSONDecodeError(line_str, e) from e
+ continue
+
+ except subprocess.SubprocessError:
+ pass
+
+ process.wait()
+ if process.returncode is not None and process.returncode != 0:
+ stderr_output = "\n".join(stderr_lines)
+ if stderr_output and "error" in stderr_output.lower():
+ raise AgentProcessError(
+ "CLI process failed",
+ exit_code=process.returncode,
+ stderr=stderr_output,
+ )
+
+ def is_connected(self) -> bool:
+ process = self._process
+ return process is not None and process.returncode is None
diff --git a/vet/imbue_core/agents/configs.py b/vet/imbue_core/agents/configs.py
@@ -0,0 +1,123 @@
+from pathlib import Path
+from typing import Any
+from typing import assert_never
+
+from vet.imbue_core.agents.llm_apis.anthropic_api import AnthropicModelName
+from vet.imbue_core.agents.llm_apis.anthropic_api import count_anthropic_tokens
+from vet.imbue_core.agents.llm_apis.common import get_model_max_context_length
+from vet.imbue_core.agents.llm_apis.constants import approximate_token_count
+from vet.imbue_core.agents.llm_apis.data_types import ModelStr
+from vet.imbue_core.agents.llm_apis.mock_api import MY_MOCK_MODEL_INFO
+from vet.imbue_core.agents.llm_apis.openai_api import OpenAIModelName
+from vet.imbue_core.agents.llm_apis.openai_api import count_openai_tokens
+from vet.imbue_core.language_model_mode import LanguageModelMode
+from vet.imbue_core.pydantic_serialization import SerializableModel
+
+
+class LanguageModelGenerationConfig(SerializableModel):
+ model_name: ModelStr = OpenAIModelName.GPT_4O_2024_08_06
+ # this should almost always be None (you dont want to save your cache path into the hammer invocation data!)
+ cache_path: Path | None = None
+ count_tokens_cache_path: Path | None = None
+ is_prompt_debugging_enabled: bool = False
+
+ # If true, the LLM API will cache the inputs to the LLM call as well as the outputs, which makes prompt diffing easier.
+ is_caching_inputs: bool = False
+
+ # if this is set, the LLM API will return ONLY cached responses
+ is_running_offline: bool = False
+
+ # if set, the LLM API will return log probabilities for the output tokens (if supported by the model)
+ is_using_logprobs: bool = False
+
+ # Retry configuration
+ retry_jitter_factor: float = 0.5
+
+ def model_post_init(self, __context: Any) -> None:
+ super().model_post_init(__context)
+ # FIXME: do proper validation
+ if self.cache_path is None and self.is_caching_inputs:
+ raise ValueError("cache_path must be provided if is_caching_inputs is True")
+
+ def count_tokens(self, text: str) -> int:
+ """Count tokens in the given text using the model's tokenizer."""
+ if self.model_name in (v for v in OpenAIModelName):
+ return count_openai_tokens(text, self.model_name)
+ if self.model_name in (v for v in AnthropicModelName):
+ return count_anthropic_tokens(text)
+ return approximate_token_count(text)
+
+ def get_max_context_length(self) -> int:
+ """Get the maximum context length for this model."""
+ return get_model_max_context_length(self.model_name)
+
+ def is_custom_model(self) -> bool:
+ """Return True if this is a custom/user-defined model.
+
+ Custom models use approximate token counting since there's no mechanism
+ for defining a tokenizer for them.
+ """
+ return False
+
+
+class OpenAICompatibleModelConfig(LanguageModelGenerationConfig):
+ """Configuration for custom models using OpenAI-compatible APIs (e.g., Ollama, local LLMs)."""
+
+ custom_base_url: str
+ custom_api_key_env: str
+ custom_context_window: int
+ custom_max_output_tokens: int
+
+ def count_tokens(self, text: str) -> int:
+ """Count tokens using approximation since we don't have access to the model's tokenizer."""
+ return approximate_token_count(text)
+
+ def get_max_context_length(self) -> int:
+ """Get the maximum context length for this model."""
+ return self.custom_context_window
+
+ def is_custom_model(self) -> bool:
+ """Return True if this is a custom/user-defined model.
+
+ Custom models use approximate token counting since there's no mechanism
+ for defining a tokenizer for them.
+ """
+ # TODO: Support custom tokenizers with custom models.
+ return True
+
+
+class MockedLanguageModelGenerationConfig(LanguageModelGenerationConfig):
+ model_name: ModelStr = MY_MOCK_MODEL_INFO.model_name
+ is_running_offline: bool = True
+ mock_responses_path: Path
+
+
+def create_safe_llm_config(
+ llm_name: ModelStr, mode: LanguageModelMode, cache_path: Path | None = None
+) -> LanguageModelGenerationConfig:
+ match mode:
+ case LanguageModelMode.LIVE:
+ assert cache_path is None
+ language_model_config = LanguageModelGenerationConfig(model_name=llm_name)
+ case LanguageModelMode.OFFLINE:
+ assert cache_path is not None
+ language_model_config = LanguageModelGenerationConfig(
+ model_name=llm_name,
+ is_running_offline=True,
+ is_caching_inputs=True,
+ cache_path=cache_path,
+ )
+ case LanguageModelMode.UPDATE_SNAPSHOT:
+ assert cache_path is not None
+ language_model_config = LanguageModelGenerationConfig(
+ model_name=llm_name, is_caching_inputs=True, cache_path=cache_path
+ )
+ case LanguageModelMode.MOCKED:
+ assert cache_path is not None
+ language_model_config = MockedLanguageModelGenerationConfig(
+ model_name=llm_name, mock_responses_path=cache_path
+ )
+ case _ as unreachable:
+ assert_never(unreachable) # pyre-ignore[6]: pyre doesn't understand enums
+ assert False # because pyre doesn't really understand assert_never, either
+ return language_model_config
diff --git a/imbue_core/imbue_core/agents/llm_apis/__init__.py b/vet/imbue_core/agents/llm_apis/__init__.py
diff --git a/vet/imbue_core/agents/llm_apis/anthropic_api.py b/vet/imbue_core/agents/llm_apis/anthropic_api.py
@@ -0,0 +1,818 @@
+import enum
+import inspect
+from contextlib import contextmanager
+from functools import lru_cache
+from pathlib import Path
+from types import FrameType
+from typing import AsyncGenerator
+from typing import Final
+from typing import Iterator
+
+import anthropic
+import httpx
+from anthropic._types import NOT_GIVEN
+from anthropic.types import CacheControlEphemeralParam
+from anthropic.types import MessageParam
+from anthropic.types import TextBlockParam
+from loguru import logger
+from pydantic.functional_validators import field_validator
+import tiktoken
+
+from vet.imbue_core.agents.llm_apis.anthropic_data_types import AnthropicCachingInfo
+from vet.imbue_core.agents.llm_apis.anthropic_data_types import AnthropicModelInfo
+from vet.imbue_core.agents.llm_apis.api_utils import convert_prompt_to_messages
+from vet.imbue_core.agents.llm_apis.api_utils import (
+ create_costed_language_model_response_for_single_result,
+)
+from vet.imbue_core.agents.llm_apis.data_types import CachedCountTokensResponse
+from vet.imbue_core.agents.llm_apis.data_types import CachingInfo
+from vet.imbue_core.agents.llm_apis.data_types import CostedLanguageModelResponse
+from vet.imbue_core.agents.llm_apis.data_types import CountTokensInputs
+from vet.imbue_core.agents.llm_apis.data_types import CountTokensResponse
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelGenerationParams
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelResponseUsage
+from vet.imbue_core.agents.llm_apis.data_types import ResponseStopReason
+from vet.imbue_core.agents.llm_apis.errors import BadAPIRequestError
+from vet.imbue_core.agents.llm_apis.errors import LanguageModelInvalidModelNameError
+from vet.imbue_core.agents.llm_apis.errors import MissingAPIKeyError
+from vet.imbue_core.agents.llm_apis.errors import NewSeedRetriableLanguageModelError
+from vet.imbue_core.agents.llm_apis.errors import SafelyRetriableTransientLanguageModelError
+from vet.imbue_core.agents.llm_apis.errors import TransientLanguageModelError
+from vet.imbue_core.agents.llm_apis.errors import UnsetCachePathError
+from vet.imbue_core.agents.llm_apis.language_model_api import LanguageModelAPI
+from vet.imbue_core.agents.llm_apis.models import ModelInfo
+from vet.imbue_core.agents.llm_apis.stream import LanguageModelStreamDeltaEvent
+from vet.imbue_core.agents.llm_apis.stream import LanguageModelStreamEndEvent
+from vet.imbue_core.agents.llm_apis.stream import LanguageModelStreamEvent
+from vet.imbue_core.agents.llm_apis.stream import LanguageModelStreamStartEvent
+from vet.imbue_core.async_monkey_patches import log_exception
+from vet.imbue_core.caching import AsyncCache
+from vet.imbue_core.frozen_utils import FrozenDict
+from vet.imbue_core.frozen_utils import FrozenMapping
+from vet.imbue_core.itertools import only
+from vet.imbue_core.nested_evolver import assign
+from vet.imbue_core.nested_evolver import chill
+from vet.imbue_core.nested_evolver import evolver
+from vet.imbue_core.secrets_utils import get_secret
+
+
+class AnthropicModelName(enum.StrEnum):
+ CLAUDE_3_HAIKU_2024_03_07 = "claude-3-haiku-20240307"
+ CLAUDE_3_OPUS_2024_02_29 = "claude-3-opus-20240229"
+ CLAUDE_3_5_SONNET_2024_06_20 = "claude-3-5-sonnet-20240620"
+ CLAUDE_3_5_SONNET_2024_10_22 = "claude-3-5-sonnet-20241022"
+ CLAUDE_3_5_HAIKU_2024_10_22 = "claude-3-5-haiku-20241022"
+ CLAUDE_3_7_SONNET_2025_02_19 = "claude-3-7-sonnet-20250219"
+ CLAUDE_4_OPUS_2025_05_14 = "claude-opus-4-20250514"
+ CLAUDE_4_1_OPUS_2025_08_05 = "claude-opus-4-1-20250805"
+ CLAUDE_4_SONNET_2025_05_14 = "claude-sonnet-4-20250514"
+ CLAUDE_4_5_SONNET_2025_09_29 = "claude-sonnet-4-5-20250929"
+ CLAUDE_4_5_HAIKU_2025_10_01 = "claude-haiku-4-5-20251001"
+ CLAUDE_4_5_OPUS_2025_11_01 = "claude-opus-4-5-20251101"
+ # the same as above but with the token limit and cost per token for the 1M token limit
+ # TODO: combine these and add ability for token costs to be nonlinear
+ # FIXME: this is an exception where the model name is not the same as the model name in the API
+ CLAUDE_4_SONNET_2025_05_14_LONG = "claude-sonnet-4-20250514-long"
+ CLAUDE_4_5_SONNET_2025_09_29_LONG = "claude-sonnet-4-5-20250929-long"
+
+ # the following are 'retired' and are no longer available: https://docs.claude.com/en/docs/about-claude/model-deprecations
+ # CLAUDE_2_1 = "claude-2.1"
+ # CLAUDE_2 = "claude-2"
+ # CLAUDE_3_SONNET_2024_02_29 = "claude-3-sonnet-20240229"
+
+
+# Basic info is available at https://docs.anthropic.com/claude/reference/models
+# Rate limits for Anthropic models are available on our dashboard: https://console.anthropic.com/settings/limits
+# (we have a custom plan, so the public docs don't reflect our actual rate limits)
+# Prompt caching pricing is available at https://docs.claude.com/en/docs/build-with-claude/prompt-caching#pricing
+# NOTE: as of 2025-06-04, there are some models that don't have rate limits set in our dashboard
+ANTHROPIC_MODEL_INFO_BY_NAME: FrozenMapping[AnthropicModelName, ModelInfo] = FrozenDict(
+ {
+ AnthropicModelName.CLAUDE_3_HAIKU_2024_03_07: ModelInfo(
+ model_name=AnthropicModelName.CLAUDE_3_HAIKU_2024_03_07,
+ cost_per_input_token=0.25 / 1_000_000,
+ cost_per_output_token=1.25 / 1_000_000,
+ max_input_tokens=200_000,
+ max_output_tokens=4096,
+ rate_limit_req=4000 / 60, # 4000 RPM = 66.67 RPS
+ rate_limit_tok=4_000_000 / 60,
+ rate_limit_output_tok=800_000 / 60,
+ provider_specific_info=AnthropicModelInfo(
+ cost_per_5m_cache_write_token=0.3 / 1_000_000,
+ cost_per_1h_cache_write_token=0.5 / 1_000_000,
+ cost_per_cache_read_token=0.03 / 1_000_000,
+ ),
+ ),
+ AnthropicModelName.CLAUDE_3_OPUS_2024_02_29: ModelInfo(
+ model_name=AnthropicModelName.CLAUDE_3_OPUS_2024_02_29,
+ cost_per_input_token=15.00 / 1_000_000,
+ cost_per_output_token=75.00 / 1_000_000,
+ max_input_tokens=200_000,
+ max_output_tokens=4096,
+ rate_limit_req=4000 / 60, # 4000 RPM = 66.67 RPS
+ rate_limit_tok=1_000_000 / 60,
+ rate_limit_output_tok=150_000 / 60,
+ provider_specific_info=AnthropicModelInfo(
+ cost_per_5m_cache_write_token=18.75 / 1_000_000,
+ cost_per_1h_cache_write_token=30 / 1_000_000,
+ cost_per_cache_read_token=1.5 / 1_000_000,
+ ),
+ ),
+ AnthropicModelName.CLAUDE_3_5_SONNET_2024_06_20: ModelInfo(
+ model_name=AnthropicModelName.CLAUDE_3_5_SONNET_2024_06_20,
+ cost_per_input_token=3.00 / 1_000_000,
+ cost_per_output_token=15.00 / 1_000_000,
+ max_input_tokens=200_000,
+ max_output_tokens=4096,
+ rate_limit_req=5000 / 60, # 5000 RPM = 83.33 RPS
+ rate_limit_tok=8_000_000 / 60,
+ rate_limit_output_tok=1_600_000 / 60,
+ provider_specific_info=AnthropicModelInfo(
+ cost_per_5m_cache_write_token=3.75 / 1_000_000,
+ cost_per_1h_cache_write_token=6 / 1_000_000,
+ cost_per_cache_read_token=0.3 / 1_000_000,
+ ),
+ ),
+ AnthropicModelName.CLAUDE_3_5_SONNET_2024_10_22: ModelInfo(
+ model_name=AnthropicModelName.CLAUDE_3_5_SONNET_2024_10_22,
+ cost_per_input_token=3.00 / 1_000_000,
+ cost_per_output_token=15.00 / 1_000_000,
+ max_input_tokens=200_000,
+ max_output_tokens=8192,
+ rate_limit_req=5000 / 60, # 5000 RPM = 83.33 RPS
+ rate_limit_tok=8_000_000 / 60,
+ rate_limit_output_tok=400_000 / 60,
+ ),
+ AnthropicModelName.CLAUDE_3_5_HAIKU_2024_10_22: ModelInfo(
+ model_name=AnthropicModelName.CLAUDE_3_5_HAIKU_2024_10_22,
+ cost_per_input_token=1.00 / 1_000_000,
+ cost_per_output_token=5.00 / 1_000_000,
+ max_input_tokens=200_000,
+ max_output_tokens=8192,
+ rate_limit_req=4000 / 60, # 4000 RPM = 66.67 RPS
+ rate_limit_tok=4_000_000 / 60,
+ rate_limit_output_tok=800_000 / 60,
+ provider_specific_info=AnthropicModelInfo(
+ cost_per_5m_cache_write_token=1 / 1_000_000,
+ cost_per_1h_cache_write_token=1.6 / 1_000_000,
+ cost_per_cache_read_token=0.08 / 1_000_000,
+ ),
+ ),
+ AnthropicModelName.CLAUDE_3_7_SONNET_2025_02_19: ModelInfo(
+ model_name=AnthropicModelName.CLAUDE_3_7_SONNET_2025_02_19,
+ cost_per_input_token=3.00 / 1_000_000,
+ cost_per_output_token=15.00 / 1_000_000,
+ max_input_tokens=200_000,
+ max_output_tokens=8192,
+ rate_limit_req=None, # Currently no limit set in our dashboard
+ rate_limit_tok=2_000_000 / 60,
+ rate_limit_output_tok=400_000 / 60,
+ provider_specific_info=AnthropicModelInfo(
+ cost_per_5m_cache_write_token=3.75 / 1_000_000,
+ cost_per_1h_cache_write_token=6 / 1_000_000,
+ cost_per_cache_read_token=0.3 / 1_000_000,
+ ),
+ ),
+ AnthropicModelName.CLAUDE_4_OPUS_2025_05_14: ModelInfo(
+ model_name=AnthropicModelName.CLAUDE_4_OPUS_2025_05_14,
+ cost_per_input_token=15.00 / 1_000_000,
+ cost_per_output_token=75.00 / 1_000_000,
+ max_input_tokens=200_000,
+ max_output_tokens=32_000,
+ rate_limit_req=4000 / 60,
+ rate_limit_tok=2_000_000 / 60,
+ rate_limit_output_tok=400_000 / 60,
+ provider_specific_info=AnthropicModelInfo(
+ cost_per_5m_cache_write_token=18.75 / 1_000_000,
+ cost_per_1h_cache_write_token=30 / 1_000_000,
+ cost_per_cache_read_token=1.5 / 1_000_000,
+ ),
+ ),
+ AnthropicModelName.CLAUDE_4_1_OPUS_2025_08_05: ModelInfo(
+ model_name=AnthropicModelName.CLAUDE_4_1_OPUS_2025_08_05,
+ cost_per_input_token=15.00 / 1_000_000,
+ cost_per_output_token=75.00 / 1_000_000,
+ max_input_tokens=200_000,
+ max_output_tokens=32_000,
+ rate_limit_req=4000 / 60,
+ rate_limit_tok=2_000_000 / 60,
+ rate_limit_output_tok=400_000 / 60,
+ provider_specific_info=AnthropicModelInfo(
+ cost_per_5m_cache_write_token=18.75 / 1_000_000,
+ cost_per_1h_cache_write_token=30 / 1_000_000,
+ cost_per_cache_read_token=1.5 / 1_000_000,
+ ),
+ ),
+ AnthropicModelName.CLAUDE_4_5_OPUS_2025_11_01: ModelInfo(
+ model_name=AnthropicModelName.CLAUDE_4_5_OPUS_2025_11_01,
+ cost_per_input_token=5.00 / 1_000_000,
+ cost_per_output_token=25.00 / 1_000_000,
+ max_input_tokens=200_000,
+ max_output_tokens=64_000,
+ rate_limit_req=4000 / 60,
+ rate_limit_tok=2_000_000 / 60,
+ rate_limit_output_tok=400_000 / 60,
+ provider_specific_info=AnthropicModelInfo(
+ cost_per_5m_cache_write_token=6.25 / 1_000_000,
+ cost_per_1h_cache_write_token=10 / 1_000_000,
+ cost_per_cache_read_token=0.5 / 1_000_000,
+ ),
+ ),
+ AnthropicModelName.CLAUDE_4_SONNET_2025_05_14: ModelInfo(
+ model_name=AnthropicModelName.CLAUDE_4_SONNET_2025_05_14,
+ cost_per_input_token=3.00 / 1_000_000,
+ cost_per_output_token=15.00 / 1_000_000,
+ max_input_tokens=200_000,
+ max_output_tokens=64_000,
+ rate_limit_req=None, # Currently no limit set in our dashboard
+ rate_limit_tok=2_000_000 / 60,
+ rate_limit_output_tok=400_000 / 60,
+ provider_specific_info=AnthropicModelInfo(
+ cost_per_5m_cache_write_token=3.75 / 1_000_000,
+ cost_per_1h_cache_write_token=6 / 1_000_000,
+ cost_per_cache_read_token=0.3 / 1_000_000,
+ ),
+ ),
+ AnthropicModelName.CLAUDE_4_5_SONNET_2025_09_29: ModelInfo(
+ model_name=AnthropicModelName.CLAUDE_4_5_SONNET_2025_09_29,
+ cost_per_input_token=3.00 / 1_000_000,
+ cost_per_output_token=15.00 / 1_000_000,
+ max_input_tokens=200_000,
+ max_output_tokens=64_000,
+ rate_limit_req=None, # Currently no limit set in our dashboard
+ rate_limit_tok=2_000_000 / 60,
+ rate_limit_output_tok=400_000 / 60,
+ provider_specific_info=AnthropicModelInfo(
+ cost_per_5m_cache_write_token=3.75 / 1_000_000,
+ cost_per_1h_cache_write_token=6 / 1_000_000,
+ cost_per_cache_read_token=0.3 / 1_000_000,
+ ),
+ ),
+ AnthropicModelName.CLAUDE_4_5_HAIKU_2025_10_01: ModelInfo(
+ model_name=AnthropicModelName.CLAUDE_4_5_HAIKU_2025_10_01,
+ cost_per_input_token=1.00 / 1_000_000,
+ cost_per_output_token=5.00 / 1_000_000,
+ max_input_tokens=200_000,
+ max_output_tokens=64_000,
+ rate_limit_req=4_000 / 60,
+ rate_limit_tok=4_000_000 / 60,
+ rate_limit_output_tok=800_000 / 60,
+ provider_specific_info=AnthropicModelInfo(
+ cost_per_5m_cache_write_token=1.25 / 1_000_000,
+ cost_per_1h_cache_write_token=2.0 / 1_000_000,
+ cost_per_cache_read_token=0.1 / 1_000_000,
+ ),
+ ),
+ AnthropicModelName.CLAUDE_4_SONNET_2025_05_14_LONG: ModelInfo(
+ model_name=AnthropicModelName.CLAUDE_4_SONNET_2025_05_14_LONG,
+ # the first 200_000 input tokens use the rates above, and the next up to 800_000 use the rate 6.0 / 1_000_000.
+ # thus the maximum average cost per input token is (3.0 * 200_000 + 6.0 * 800_000) / 1_000_000 = 5.4 per 1_000_000.
+ # (all output tokens may be past 200_000 input tokens, so the max average cost there is just the cost for tokens after 200_000)
+ cost_per_input_token=5.40 / 1_000_000,
+ cost_per_output_token=22.50 / 1_000_000,
+ max_input_tokens=1_000_000,
+ max_output_tokens=64_000,
+ rate_limit_req=None, # Currently no limit set in our dashboard
+ rate_limit_tok=1_000_000 / 60, # <-- yeah they let us have one (1) 1M request per minute
+ rate_limit_output_tok=200_000 / 60,
+ ),
+ AnthropicModelName.CLAUDE_4_5_SONNET_2025_09_29_LONG: ModelInfo(
+ model_name=AnthropicModelName.CLAUDE_4_5_SONNET_2025_09_29_LONG,
+ # the first 200_000 input tokens use the rates above, and the next up to 800_000 use the rate 6.0 / 1_000_000.
+ # thus the maximum average cost per input token is (3.0 * 200_000 + 6.0 * 800_000) / 1_000_000 = 5.4 per 1_000_000.
+ # (all output tokens may be past 200_000 input tokens, so the max average cost there is just the cost for tokens after 200_000)
+ cost_per_input_token=5.40 / 1_000_000,
+ cost_per_output_token=22.50 / 1_000_000,
+ max_input_tokens=1_000_000,
+ max_output_tokens=64_000,
+ rate_limit_req=None, # Currently no limit set in our dashboard
+ rate_limit_tok=1_000_000 / 60, # <-- yeah they let us have one (1) 1M request per minute
+ rate_limit_output_tok=200_000 / 60,
+ ),
+ }
+)
+
+
+_ROLE_TO_ANTHROPIC_ROLE: Final[FrozenMapping[str, str]] = FrozenDict(
+ {
+ "HUMAN": "user",
+ "ASSISTANT": "assistant",
+ "USER": "user",
+ "USER_CACHED": "user",
+ "SYSTEM": "system",
+ "SYSTEM_CACHED": "system",
+ }
+)
+
+_ANTHROPIC_STOP_REASON_TO_STOP_REASON: Final[FrozenMapping[str, ResponseStopReason]] = FrozenDict(
+ {
+ "end_turn": ResponseStopReason.END_TURN,
+ "max_tokens": ResponseStopReason.MAX_TOKENS,
+ "stop_sequence": ResponseStopReason.STOP_SEQUENCE,
+ "refusal": ResponseStopReason.CONTENT_FILTER,
+ }
+)
+
+_ANTHROPIC_BETA_PROMPT_CACHING = "prompt-caching-2024-07-31"
+_ANTHROPIC_BETA_OAUTH = "oauth-2025-04-20"
+
+
+@lru_cache(maxsize=1)
+def get_anthropic_tokenizer() -> tiktoken.Encoding:
+ """Use cl100k_base encoding as an approximation for Claude tokenization.
+
+ Modern Anthropic SDK does not expose a tokenizer directly and instead
+ relies on API calls to count tokens. Using that implementation would
+ put HTTP calls in our `count_tokens` implementation which would be tricky
+ as the method would have to be async or block the event loop.
+
+ Instead, we use tiktoken's cl100k_base encoding (used by GPT-4) as a
+ reasonable approximation. This allows us to count tokens without making
+ HTTP requests at the cost of slightly inaccurate token counts.
+ """
+ return tiktoken.get_encoding("cl100k_base")
+
+
+def count_anthropic_tokens(text: str) -> int:
+ return int(len(get_anthropic_tokenizer().encode(text)) * 1.1)
+
+
+SystemMessageParam = TextBlockParam
+
+
+def _convert_prompt_to_anthropic_messages(
+ prompt: str,
+) -> tuple[list[MessageParam], list[SystemMessageParam] | None]:
+ """Converts a prompt into list of non-system (user/assistant) messages and the optional system prompt."""
+ non_system_messages = []
+ system_messages = []
+ for msg in convert_prompt_to_messages(prompt, is_cache_role_preserved=True):
+ role = _ROLE_TO_ANTHROPIC_ROLE[msg.role]
+ if msg.role == "SYSTEM_CACHED":
+ system_messages.append(
+ {
+ "type": "text",
+ "text": msg.content,
+ "cache_control": {"type": "ephemeral"},
+ },
+ )
+ elif role == "system":
+ system_messages.append(
+ {
+ "type": "text",
+ "text": msg.content,
+ },
+ )
+ elif role == "USER_CACHED":
+ non_system_messages.append(
+ MessageParam( # pyre-fixme[28]: MessageParam doesn't have cache_control
+ content=msg.content,
+ role="user",
+ cache_control=CacheControlEphemeralParam(type="ephemeral"),
+ )
+ )
+ else:
+ non_system_messages.append(MessageParam(content=msg.content, role=role)) # type: ignore
+
+ if len(system_messages) > 1:
+ logger.debug("system_messages: {}", system_messages)
+ raise ValueError(f"Anthropic API supports only 0 or 1 system message; got {len(system_messages)}.")
+
+ if len(non_system_messages) == 0:
+ system_messages = None
+
+ return non_system_messages, system_messages
+
+
+@contextmanager
+def _anthropic_exception_manager() -> Iterator[None]:
+ """Simple context manager for parsing Anthropic API exceptions."""
+ # ref
+ try:
+ yield
+ except anthropic.InternalServerError as e:
+ # this can be caused by either malformed requests or transient errors, so play it safe and retry
+ raise TransientLanguageModelError(str(e)) from e
+ except anthropic.BadRequestError as e:
+ logger.info("BadAPIRequestError {e}", e=e)
+ raise BadAPIRequestError(str(e)) from e
+ except TypeError as e:
+ logger.info("Type error calling Anthropic API: {e}", e=e)
+ raise BadAPIRequestError(str(e)) from e
+ except anthropic.APIConnectionError as e:
+ raise TransientLanguageModelError(str(e)) from e
+ except anthropic.RateLimitError as e:
+ extra_header_keys = [x for x in e.response.headers.keys() if x.startswith("anthropic-")]
+ extra_data = ", ".join([f"{key}={e.response.headers[key]}" for key in extra_header_keys])
+ extra_info = f"Rate limit data: {extra_data}"
+ raise TransientLanguageModelError(extra_info) from e
+ except anthropic.APIStatusError as e:
+ if "overloaded_error" in str(e):
+ raise SafelyRetriableTransientLanguageModelError(str(e)) from e
+ if "internal server error" in str(e).lower():
+ raise SafelyRetriableTransientLanguageModelError(str(e)) from e
+ # this happens when anthropic provides us open source code and then feels bad about it
+ # anthropic.APIStatusError: {'type': 'error', 'error': {'details': None, 'type': 'invalid_request_error', 'message': 'Output blocked by content filtering policy'}}
+ if e.message == "Output blocked by content filtering policy":
+ raise NewSeedRetriableLanguageModelError(e)
+ logger.info(str(e))
+ if e.status_code == 409 or e.status_code >= 500:
+ raise TransientLanguageModelError(str(e)) from e
+ raise
+ except httpx.RemoteProtocolError as e:
+ logger.info(str(e))
+ raise TransientLanguageModelError("httpx.RemoteProtocolError") from e
+ except (BadAPIRequestError, TransientLanguageModelError, MissingAPIKeyError):
+ # we already raised this error ourselves earlier, so we don't need to mark it as unknown
+ raise
+ except Exception as e:
+ # we catch TransientLanguageModelError later to retry it, but we still want to log it so it's not silent
+ log_exception(
+ e,
+ "Failed to generate output from Anthropic, unknown error of type {type_name}",
+ type_name=type(e).__name__,
+ )
+ raise TransientLanguageModelError("Unknown error") from e
+
+
+class MissingCachingInfoError(Exception):
+ pass
+
+
+class AnthropicAPI(LanguageModelAPI):
+ model_name: AnthropicModelName = AnthropicModelName.CLAUDE_4_SONNET_2025_05_14
+ is_conversational: bool = True
+
+ # Anthropic specific args
+ # unclear what the timeout ought to be actually, set to 1 minute for now because their default of 10 minutes seems insane
+ timeout: float = 60.0
+ max_retries: int = 0
+ count_tokens_cache_path: Path | None = None
+
+ @field_validator("model_name") # pyre-ignore[56]: pyre doesn't understand pydantic
+ @classmethod
+ def validate_model_name(cls, v: str) -> str:
+ if v not in ANTHROPIC_MODEL_INFO_BY_NAME:
+ raise LanguageModelInvalidModelNameError(v, cls.__name__, list(ANTHROPIC_MODEL_INFO_BY_NAME))
+ return v
+
+ @property
+ def model_info(self) -> ModelInfo:
+ return ANTHROPIC_MODEL_INFO_BY_NAME[self.model_name]
+
+ def _get_sync_client(self) -> anthropic.Anthropic:
+ api_key, auth_token = _get_api_key_or_auth_token()
+ if api_key:
+ return anthropic.Anthropic(api_key=api_key)
+ else:
+ return anthropic.Anthropic(
+ auth_token=auth_token,
+ default_headers={"anthropic-beta": _ANTHROPIC_BETA_OAUTH},
+ )
+
+ def _get_client(self) -> anthropic.AsyncAnthropic:
+ api_key, auth_token = _get_api_key_or_auth_token()
+ if api_key:
+ return anthropic.AsyncAnthropic(
+ api_key=api_key,
+ max_retries=self.max_retries,
+ timeout=self.timeout,
+ default_headers={"anthropic-beta": _ANTHROPIC_BETA_PROMPT_CACHING},
+ )
+ else:
+ return anthropic.AsyncAnthropic(
+ auth_token=auth_token,
+ max_retries=self.max_retries,
+ timeout=self.timeout,
+ default_headers={"anthropic-beta": f"{_ANTHROPIC_BETA_PROMPT_CACHING},{_ANTHROPIC_BETA_OAUTH}"},
+ )
+
+ async def _call_api(
+ self,
+ prompt: str,
+ params: LanguageModelGenerationParams,
+ network_failure_count: int = 0,
+ ) -> CostedLanguageModelResponse:
+ assert (
+ params.count == 1
+ ), "Anthropic API only supports count=1. It is possible to hack around this by using a for loop, but doesn't seem worth it right now."
+
+ non_system_messages, system_messages = _convert_prompt_to_anthropic_messages(prompt)
+
+ with _anthropic_exception_manager():
+ async with self._get_client() as client:
+ if params.max_tokens is None:
+ # NOTE: anthropic's API REQUIRES you to provide this, if you don't pass it in we just set it to the maximum possible
+
+ # use the evolver method of updating instead
+ # params.max_tokens = self.model_info.max_output_tokens
+ param_with_max_tokens_evolver = evolver(params)
+ assign(
+ param_with_max_tokens_evolver.max_tokens,
+ lambda: self.model_info.max_output_tokens,
+ )
+ params = chill(param_with_max_tokens_evolver)
+ assert params.max_tokens is not None, "max_tokens must be provided for Anthropic API"
+
+ if self.model_name in (
+ AnthropicModelName.CLAUDE_4_5_SONNET_2025_09_29_LONG,
+ AnthropicModelName.CLAUDE_4_SONNET_2025_05_14_LONG,
+ ):
+ # FIXME: Fix this once this is no longer beta or as this becomes required for more models
+ # Map the name back to the actual model name for the API call
+ if self.model_name == AnthropicModelName.CLAUDE_4_5_SONNET_2025_09_29_LONG:
+ model_name = AnthropicModelName.CLAUDE_4_5_SONNET_2025_09_29
+ elif self.model_name == AnthropicModelName.CLAUDE_4_SONNET_2025_05_14_LONG:
+ model_name = AnthropicModelName.CLAUDE_4_SONNET_2025_05_14
+ else:
+ assert False, "unreachable"
+ api_result = await client.beta.messages.create(
+ messages=non_system_messages,
+ stop_sequences=([params.stop] if params.stop is not None else NOT_GIVEN),
+ model=model_name,
+ temperature=params.temperature,
+ system=prepend_claude_code_system_prompt(system_messages),
+ max_tokens=params.max_tokens,
+ betas=["context-1m-2025-08-07"],
+ )
+ detailed_caching_data = AnthropicCachingInfo(
+ written_5m=api_result.usage.cache_creation.ephemeral_5m_input_tokens,
+ written_1h=api_result.usage.cache_creation.ephemeral_1h_input_tokens,
+ )
+ else:
+ api_result = await client.messages.create(
+ messages=non_system_messages,
+ stop_sequences=([params.stop] if params.stop is not None else NOT_GIVEN),
+ model=self.model_name,
+ temperature=params.temperature,
+ system=prepend_claude_code_system_prompt(system_messages),
+ max_tokens=params.max_tokens,
+ )
+ detailed_caching_data = AnthropicCachingInfo(
+ written_5m=api_result.usage.cache_creation.ephemeral_5m_input_tokens,
+ written_1h=api_result.usage.cache_creation.ephemeral_1h_input_tokens,
+ )
+ text = only(api_result.content).text
+ if api_result.stop_reason:
+ stop_reason = _ANTHROPIC_STOP_REASON_TO_STOP_REASON.get(
+ str(api_result.stop_reason), ResponseStopReason.NONE
+ )
+ else:
+ stop_reason = ResponseStopReason.NONE
+ if params.stop and stop_reason == ResponseStopReason.STOP_SEQUENCE:
+ text += params.stop
+ logger.trace(text)
+
+ prompt_tokens = api_result.usage.input_tokens
+ completion_tokens = api_result.usage.output_tokens # type: ignore
+ caching_info = CachingInfo(
+ read_from_cache=api_result.usage.cache_read_input_tokens,
+ provider_specific_data=detailed_caching_data,
+ )
+ dollars_used = self.calculate_cost(prompt_tokens, completion_tokens, caching_info)
+ logger.trace("Dollars used: {dollars_used}", dollars_used=dollars_used)
+
+ return create_costed_language_model_response_for_single_result(
+ text=text,
+ prompt_tokens=prompt_tokens,
+ completion_tokens=completion_tokens,
+ stop_reason=stop_reason,
+ network_failure_count=network_failure_count,
+ dollars_used=dollars_used,
+ caching_info=caching_info,
+ )
+
+ async def _get_api_stream(
+ self,
+ prompt: str,
+ params: LanguageModelGenerationParams,
+ ) -> AsyncGenerator[LanguageModelStreamEvent, None]:
+ non_system_messages, system_messages = _convert_prompt_to_anthropic_messages(prompt)
+ with _anthropic_exception_manager():
+ async with self._get_client() as client:
+ yield LanguageModelStreamStartEvent()
+
+ # NOTE: anthropic's API REQUIRES you to provide this, if you don't pass it in we just set it to the maximum possible
+ max_tokens = params.max_tokens if params.max_tokens is not None else self.model_info.max_output_tokens
+ assert max_tokens is not None, "max_tokens must be provided for Anthropic API"
+
+ if self.model_name in (
+ AnthropicModelName.CLAUDE_4_5_SONNET_2025_09_29_LONG,
+ AnthropicModelName.CLAUDE_4_SONNET_2025_05_14_LONG,
+ ):
+ # FIXME: Fix this once this is no longer beta or as this becomes required for more models
+ # Map the name back to the actual model name for the API call
+ if self.model_name == AnthropicModelName.CLAUDE_4_5_SONNET_2025_09_29_LONG:
+ model_name = AnthropicModelName.CLAUDE_4_5_SONNET_2025_09_29
+ elif self.model_name == AnthropicModelName.CLAUDE_4_SONNET_2025_05_14_LONG:
+ model_name = AnthropicModelName.CLAUDE_4_SONNET_2025_05_14
+ else:
+ assert False, "unreachable"
+ stream_fn = lambda **kwargs: client.beta.messages.stream(**kwargs, betas=["context-1m-2025-08-07"])
+ cache_info_maker = lambda api_result: AnthropicCachingInfo(
+ written_5m=api_result.usage.cache_creation.ephemeral_5m_input_tokens,
+ written_1h=api_result.usage.cache_creation.ephemeral_1h_input_tokens,
+ )
+ else:
+ model_name = self.model_name
+ stream_fn = lambda **kwargs: client.messages.stream(**kwargs)
+ cache_info_maker = lambda api_result: AnthropicCachingInfo(
+ written_5m=api_result.usage.cache_creation.ephemeral_5m_input_tokens,
+ written_1h=api_result.usage.cache_creation.ephemeral_1h_input_tokens,
+ )
+ async with stream_fn(
+ max_tokens=max_tokens,
+ messages=non_system_messages,
+ model=model_name,
+ stop_sequences=([params.stop] if params.stop is not None else NOT_GIVEN),
+ system=system_messages or NOT_GIVEN,
+ temperature=params.temperature,
+ ) as stream:
+ async for text_delta in stream.text_stream:
+ yield LanguageModelStreamDeltaEvent(delta=text_delta)
+
+ final_message = await stream.get_final_message()
+ text = only(final_message.content).text
+ stop_reason = (
+ final_message.stop_reason if final_message.stop_reason is not None else ResponseStopReason.NONE
+ )
+ if params.stop and stop_reason == ResponseStopReason.STOP_SEQUENCE:
+ yield LanguageModelStreamDeltaEvent(delta=params.stop)
+ text += params.stop
+ logger.trace(text)
+
+ prompt_tokens = final_message.usage.input_tokens
+ # useful to confirm that the cache is actually being hit
+ logger.debug(
+ "Used this many cached read tokens: {cached_tokens}",
+ cached_tokens=final_message.usage.cache_read_input_tokens,
+ )
+ completion_tokens = final_message.usage.output_tokens
+ caching_info = CachingInfo(
+ read_from_cache=final_message.usage.cache_read_input_tokens,
+ provider_specific_data=cache_info_maker(final_message),
+ )
+ dollars_used = self.calculate_cost(prompt_tokens, completion_tokens, caching_info)
+
+ if final_message.stop_reason:
+ stop_reason = _ANTHROPIC_STOP_REASON_TO_STOP_REASON.get(
+ str(final_message.stop_reason), ResponseStopReason.NONE
+ )
+ else:
+ stop_reason = ResponseStopReason.NONE
+
+ logger.trace("Dollars used: {dollars_used}", dollars_used=dollars_used)
+ yield LanguageModelStreamEndEvent(
+ usage=LanguageModelResponseUsage(
+ prompt_tokens_used=prompt_tokens,
+ completion_tokens_used=completion_tokens,
+ dollars_used=dollars_used,
+ caching_info=caching_info,
+ ),
+ stop_reason=stop_reason,
+ )
+
+ def count_tokens(self, text: str) -> int:
+ return count_anthropic_tokens(text)
+
+ def get_count_tokens_response_cache(self) -> AsyncCache[CachedCountTokensResponse]:
+ if self.count_tokens_cache_path is None:
+ raise UnsetCachePathError()
+ return AsyncCache(self.count_tokens_cache_path, CachedCountTokensResponse)
+
+ async def check_count_tokens_cache(self, cache_key: str) -> CountTokensResponse | None:
+ return await self.check_cache_core(self.get_count_tokens_response_cache, cache_key)
+
+ async def _get_from_count_tokens_cache(
+ self, frame: FrameType | None
+ ) -> tuple[str | None, CountTokensResponse | None]:
+ return await self._get_from_cache_core(frame, lambda cr: cr, self.check_count_tokens_cache)
+
+ async def count_tokens_api(self, prompt: str, is_caching_enabled: bool) -> int:
+ """
+ Call the count tokens API. This API is free, so we don't ensure resource limits before calling it.
+ There are rate limits though: https://docs.anthropic.com/en/docs/build-with-claude/token-counting#pricing-and-rate-limits
+ """
+
+ self.assert_caching_enabled_if_offline(is_caching_enabled)
+
+ frame: FrameType | None = None
+ if is_caching_enabled:
+ frame = inspect.currentframe()
+
+ cache_key: str | None = None
+ if is_caching_enabled:
+ cache_key, cached_response = await self._get_from_count_tokens_cache(frame)
+
+ if cached_response is not None:
+ return cached_response.input_tokens
+
+ self.assert_not_offline_if_cache_miss(prompt)
+
+ non_system_messages, system_messages = _convert_prompt_to_anthropic_messages(prompt)
+
+ with _anthropic_exception_manager():
+ async with self._get_client() as client:
+ raw_response = await client.messages.count_tokens(
+ model=self.model_info.model_name,
+ messages=non_system_messages,
+ system=system_messages,
+ )
+
+ response = CountTokensResponse(input_tokens=raw_response.input_tokens)
+ result = CachedCountTokensResponse(
+ response=response,
+ inputs=(
+ CountTokensInputs(model=self.model_info.model_name, prompt=prompt)
+ if self.is_caching_inputs
+ else None
+ ),
+ )
+
+ if is_caching_enabled:
+ assert cache_key is not None
+ async with self.get_count_tokens_response_cache() as cache:
+ await cache.set(cache_key, result)
+
+ return response.input_tokens
+
+ def calculate_cost(
+ self,
+ prompt_tokens: int,
+ completion_tokens: int,
+ caching_info: CachingInfo | None = None,
+ ) -> float:
+ try:
+ # find the cost for the prompt, broken down into cache writes and regular input tokens
+
+ # if we don't have the caching info, use the basic cost model (we catch the error below)
+ if (
+ caching_info is None
+ or caching_info.provider_specific_data is None
+ or self.model_info.provider_specific_info is None
+ ):
+ raise MissingCachingInfoError(
+ f"Missing required info for more precise cost estimates; caching info: {caching_info}, model info: {self.model_info.provider_specific_info}"
+ )
+ anthropic_caching_usage = caching_info.provider_specific_data
+ assert isinstance(anthropic_caching_usage, AnthropicCachingInfo), "Expected AnthropicCachingInfo"
+ anthropic_caching_rates = self.model_info.provider_specific_info
+ assert isinstance(anthropic_caching_rates, AnthropicModelInfo), "Expected AnthropicModelInfo"
+ cache_write_5m_tokens = anthropic_caching_usage.written_5m
+ cache_write_1h_tokens = anthropic_caching_usage.written_1h
+ cache_read_tokens = caching_info.read_from_cache
+ regular_input_tokens = prompt_tokens - cache_write_5m_tokens - cache_write_1h_tokens
+
+ input_cost = (
+ cache_write_5m_tokens * anthropic_caching_rates.cost_per_5m_cache_write_token
+ + cache_write_1h_tokens * anthropic_caching_rates.cost_per_1h_cache_write_token
+ + cache_read_tokens * anthropic_caching_rates.cost_per_cache_read_token
+ + regular_input_tokens * self.model_info.cost_per_input_token
+ )
+
+ output_cost = completion_tokens * self.model_info.cost_per_output_token
+
+ return input_cost + output_cost
+
+ except MissingCachingInfoError as e:
+ logger.info("{}; using basic cost model", e)
+ return self.basic_calculate_cost(prompt_tokens, completion_tokens)
+
+
+def _get_api_key_or_auth_token() -> tuple[str | None, str | None]:
+ api_key = get_secret("ANTHROPIC_API_KEY")
+ auth_token = get_secret("ANTHROPIC_AUTH_TOKEN")
+ if not api_key and not auth_token:
+ raise MissingAPIKeyError("Neither ANTHROPIC_API_KEY nor ANTHROPIC_AUTH_TOKEN environment variable is set")
+ return api_key, auth_token
+
+
+_CLAUDE_CODE_SYSTEM_PROMPT = TextBlockParam(
+ type="text",
+ text="You are Claude Code, Anthropic's official CLI for Claude.",
+ cache_control=CacheControlEphemeralParam(type="ephemeral"),
+)
+
+
+def prepend_claude_code_system_prompt(
+ system_prompt: str | list[TextBlockParam] | None,
+) -> list[TextBlockParam]:
+ """Prepends the system prompt used by Claude Code.
+
+ When using the Claude API through Claude Pro/Max subscriptions,
+ the Claude API requires this particular system prompt to be set;
+ otherwise the request will fail.
+
+ For simplicity and consistency,
+ we always do this even when it's not strictly required,
+ (like when using the Claude API through API keys).
+ """
+ if not system_prompt:
+ return [_CLAUDE_CODE_SYSTEM_PROMPT]
+ elif isinstance(system_prompt, str):
+ return [
+ _CLAUDE_CODE_SYSTEM_PROMPT,
+ TextBlockParam(type="text", text=system_prompt),
+ ]
+ else:
+ return [_CLAUDE_CODE_SYSTEM_PROMPT] + system_prompt
diff --git a/vet/imbue_core/agents/llm_apis/anthropic_data_types.py b/vet/imbue_core/agents/llm_apis/anthropic_data_types.py
@@ -0,0 +1,15 @@
+from vet.imbue_core.pydantic_serialization import SerializableModel
+
+
+class AnthropicModelInfo(SerializableModel):
+ object_type: str = "AnthropicModelInfo"
+ cost_per_5m_cache_write_token: float
+ cost_per_1h_cache_write_token: float
+ cost_per_cache_read_token: float
+
+
+class AnthropicCachingInfo(SerializableModel):
+ object_type: str = "AnthropicCachingInfo"
+ # record info on cache writes for 5 minute and 1 hour durations
+ written_5m: int
+ written_1h: int
diff --git a/vet/imbue_core/agents/llm_apis/api_utils.py b/vet/imbue_core/agents/llm_apis/api_utils.py
@@ -0,0 +1,110 @@
+from typing import Final
+from typing import Iterable
+
+from loguru import logger
+
+from vet.imbue_core.agents.llm_apis.data_types import CachingInfo
+from vet.imbue_core.agents.llm_apis.data_types import ConversationMessage
+from vet.imbue_core.agents.llm_apis.data_types import CostedLanguageModelResponse
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelResponse
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelResponseUsage
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelResponseWithThoughts
+from vet.imbue_core.agents.llm_apis.data_types import ResponseStopReason
+from vet.imbue_core.agents.llm_apis.data_types import ThoughtResponse
+from vet.imbue_core.frozen_utils import FrozenDict
+from vet.imbue_core.frozen_utils import FrozenMapping
+
+_ROLE_TO_OPENAI_ROLE: Final[FrozenMapping] = FrozenDict(
+ {
+ "HUMAN": "user",
+ "ASSISTANT": "assistant",
+ "SYSTEM": "system",
+ "USER": "user",
+ "SYSTEM_CACHED": "system",
+ "USER_CACHED": "user",
+ }
+)
+
+
+def convert_prompt_to_messages(prompt: str, is_cache_role_preserved: bool = False) -> tuple[ConversationMessage, ...]:
+ messages = []
+ for raw_message in convert_prompt_to_openai_messages(prompt, is_cache_role_preserved):
+ messages.append(ConversationMessage(role=raw_message["role"].upper(), content=raw_message["content"]))
+ return tuple(messages)
+
+
+def convert_messages_to_prompt_template(messages: Iterable[ConversationMessage]) -> str:
+ return "\n".join(f"[ROLE={message.role.upper()}]\n{message.content}" for message in messages)
+
+
+def create_costed_language_model_response_for_single_result(
+ text: str,
+ prompt_tokens: int,
+ completion_tokens: int,
+ stop_reason: ResponseStopReason,
+ network_failure_count: int,
+ dollars_used: float,
+ thoughts: ThoughtResponse | None = None,
+ caching_info: CachingInfo | None = None,
+) -> CostedLanguageModelResponse:
+ logger.trace("dollars used: {}", dollars_used)
+ logger.trace("completion_tokens_used used: {}", completion_tokens)
+ if thoughts is None:
+ result = LanguageModelResponse(
+ text=text,
+ token_count=completion_tokens + prompt_tokens,
+ stop_reason=stop_reason,
+ network_failure_count=network_failure_count,
+ )
+ else:
+ result = LanguageModelResponseWithThoughts(
+ text=text,
+ token_count=completion_tokens + prompt_tokens,
+ stop_reason=stop_reason,
+ network_failure_count=network_failure_count,
+ thoughts=thoughts,
+ )
+
+ return CostedLanguageModelResponse(
+ usage=LanguageModelResponseUsage(
+ prompt_tokens_used=prompt_tokens,
+ completion_tokens_used=completion_tokens,
+ dollars_used=dollars_used,
+ caching_info=caching_info,
+ ),
+ responses=(result,),
+ )
+
+
+# FIXME: we should make sure that all our LLM providers use the same function here, some clean up is required
+def convert_prompt_to_openai_messages(prompt: str, is_cache_role_preserved: bool = False) -> list[dict[str, str]]:
+ prompt = prompt.lstrip()
+ assert prompt.startswith("[ROLE=")
+ prompt = prompt.replace("[ROLE=", "", 1)
+ chunks = prompt.split("\n[ROLE=")
+ messages: list[dict[str, str]] = []
+ for chunk in chunks:
+ lines = chunk.split("\n")
+ role = lines[0].strip().rstrip("]")
+ assert role in (
+ "HUMAN",
+ "ASSISTANT",
+ "USER",
+ "SYSTEM",
+ "SYSTEM_CACHED",
+ "USER_CACHED",
+ ), f"Unknown role {role} in prompt {prompt}"
+ lines.pop(0)
+ if role == "HUMAN":
+ role = "USER"
+ if len(messages) > 0:
+ messages[-1]["content"] = messages[-1]["content"] + "\n"
+ content = "\n".join(lines)
+ content = content.rstrip()
+ fixed_role = _ROLE_TO_OPENAI_ROLE[role]
+ if is_cache_role_preserved and role == "SYSTEM_CACHED":
+ fixed_role = "SYSTEM_CACHED"
+ elif is_cache_role_preserved and role == "USER_CACHED":
+ fixed_role = "USER_CACHED"
+ messages.append({"role": fixed_role, "content": content})
+ return messages
diff --git a/vet/imbue_core/agents/llm_apis/build_apis.py b/vet/imbue_core/agents/llm_apis/build_apis.py
@@ -0,0 +1,128 @@
+from pathlib import Path
+
+from vet.imbue_core.agents.configs import LanguageModelGenerationConfig
+from vet.imbue_core.agents.configs import MockedLanguageModelGenerationConfig
+from vet.imbue_core.agents.configs import OpenAICompatibleModelConfig
+from vet.imbue_core.agents.llm_apis.anthropic_api import AnthropicAPI
+from vet.imbue_core.agents.llm_apis.anthropic_api import AnthropicModelName
+from vet.imbue_core.agents.llm_apis.constants import approximate_token_count
+from vet.imbue_core.agents.llm_apis.gemini_api import GeminiAPI
+from vet.imbue_core.agents.llm_apis.gemini_api import GeminiModelName
+from vet.imbue_core.agents.llm_apis.groq_api import GroqChatAPI
+from vet.imbue_core.agents.llm_apis.groq_api import GroqSupportedModelName
+from vet.imbue_core.agents.llm_apis.language_model_api import LanguageModelAPI
+from vet.imbue_core.agents.llm_apis.mock_api import FileBasedLanguageModelMock
+from vet.imbue_core.agents.llm_apis.mock_api import MockModelName
+from vet.imbue_core.agents.llm_apis.openai_api import OpenAIChatAPI
+from vet.imbue_core.agents.llm_apis.openai_api import OpenAIModelName
+from vet.imbue_core.agents.llm_apis.openai_compatible_api import OpenAICompatibleAPI
+from vet.imbue_core.agents.llm_apis.together_api import TogetherAIModelName
+from vet.imbue_core.agents.llm_apis.together_api import TogetherAPI
+
+
+def build_language_model_from_config(
+ config: LanguageModelGenerationConfig,
+) -> LanguageModelAPI:
+ if isinstance(config, MockedLanguageModelGenerationConfig):
+ return FileBasedLanguageModelMock(cache_path=config.mock_responses_path)
+
+ if isinstance(config, OpenAICompatibleModelConfig):
+ return OpenAICompatibleAPI(
+ model_name=config.model_name,
+ base_url=config.custom_base_url,
+ api_key_env=config.custom_api_key_env,
+ context_window=config.custom_context_window,
+ max_output_tokens=config.custom_max_output_tokens,
+ cache_path=config.cache_path,
+ is_caching_inputs=config.is_caching_inputs,
+ is_running_offline=config.is_running_offline,
+ is_conversational=True,
+ retry_jitter_factor=config.retry_jitter_factor,
+ )
+
+ if config.model_name in (v for v in MockModelName):
+ return FileBasedLanguageModelMock(cache_path=config.cache_path)
+ if config.model_name in (v for v in OpenAIModelName):
+ return OpenAIChatAPI(
+ model_name=config.model_name,
+ cache_path=config.cache_path,
+ is_caching_inputs=config.is_caching_inputs,
+ is_running_offline=config.is_running_offline,
+ is_conversational=True,
+ is_using_logprobs=config.is_using_logprobs,
+ retry_jitter_factor=config.retry_jitter_factor,
+ )
+ if config.model_name in (v for v in GroqSupportedModelName):
+ return GroqChatAPI(
+ model_name=config.model_name,
+ cache_path=config.cache_path,
+ is_caching_inputs=config.is_caching_inputs,
+ is_running_offline=config.is_running_offline,
+ is_conversational=True,
+ is_using_logprobs=config.is_using_logprobs,
+ retry_jitter_factor=config.retry_jitter_factor,
+ )
+ if config.model_name in (v for v in AnthropicModelName):
+ return AnthropicAPI(
+ model_name=config.model_name,
+ cache_path=config.cache_path,
+ count_tokens_cache_path=config.count_tokens_cache_path,
+ is_caching_inputs=config.is_caching_inputs,
+ is_running_offline=config.is_running_offline,
+ is_conversational=True,
+ is_using_logprobs=config.is_using_logprobs,
+ retry_jitter_factor=config.retry_jitter_factor,
+ )
+ if config.model_name in (v for v in TogetherAIModelName):
+ return TogetherAPI(
+ model_name=config.model_name,
+ cache_path=config.cache_path,
+ # count tokens is not supported for Together API
+ # count_tokens_cache_path=config.count_tokens_cache_path,
+ is_caching_inputs=config.is_caching_inputs,
+ is_running_offline=config.is_running_offline,
+ is_conversational=True,
+ is_using_logprobs=config.is_using_logprobs,
+ retry_jitter_factor=config.retry_jitter_factor,
+ )
+ if config.model_name in (v for v in GeminiModelName):
+ return GeminiAPI(
+ model_name=config.model_name,
+ cache_path=config.cache_path,
+ count_tokens_cache_path=config.count_tokens_cache_path,
+ is_caching_inputs=config.is_caching_inputs,
+ is_running_offline=config.is_running_offline,
+ is_conversational=True,
+ is_using_logprobs=config.is_using_logprobs,
+ retry_jitter_factor=config.retry_jitter_factor,
+ )
+ # if config.model_name in MISTRAL_CHAT_MODEL_NAMES:
+ # return MistralChatAPI(
+ # model_name=config.model_name,
+ # cache_path=config.cache_path,
+ # is_conversational=True,
+ # )
+
+ raise NotImplementedError(f"{config.model_name} not supported by LanguageModelAPI")
+
+
+def build_language_model_by_name(
+ model_name: str,
+ cache_path: Path | None = None,
+ is_caching_inputs: bool = False,
+ is_using_logprobs: bool = False,
+) -> LanguageModelAPI:
+ config = LanguageModelGenerationConfig(
+ model_name=model_name,
+ cache_path=cache_path,
+ is_caching_inputs=is_caching_inputs,
+ is_using_logprobs=is_using_logprobs,
+ )
+ return build_language_model_from_config(config)
+
+
+def get_token_count_for_text_and_model(text: str, model_name: str) -> int:
+ try:
+ return build_language_model_by_name(model_name).count_tokens(text)
+ except NotImplementedError:
+ return approximate_token_count(text)
diff --git a/vet/imbue_core/agents/llm_apis/common.py b/vet/imbue_core/agents/llm_apis/common.py
@@ -0,0 +1,73 @@
+from vet.imbue_core.agents.llm_apis.anthropic_api import ANTHROPIC_MODEL_INFO_BY_NAME
+from vet.imbue_core.agents.llm_apis.anthropic_api import AnthropicModelName
+from vet.imbue_core.agents.llm_apis.gemini_api import GEMINI_MODEL_INFO_BY_NAME
+from vet.imbue_core.agents.llm_apis.gemini_api import GeminiModelName
+from vet.imbue_core.agents.llm_apis.groq_api import GroqSupportedModelName
+from vet.imbue_core.agents.llm_apis.groq_api import get_model_info as get_groq_model_info
+from vet.imbue_core.agents.llm_apis.mock_api import MY_MOCK_MODEL_INFO
+from vet.imbue_core.agents.llm_apis.models import ModelInfo
+from vet.imbue_core.agents.llm_apis.openai_api import OpenAIModelName
+from vet.imbue_core.agents.llm_apis.openai_api import (
+ get_model_info as get_openai_model_info,
+)
+from vet.imbue_core.agents.llm_apis.together_api import TOGETHERAI_MODEL_INFO_BY_NAME
+from vet.imbue_core.agents.llm_apis.together_api import TogetherAIModelName
+
+ModelName = AnthropicModelName | OpenAIModelName | GroqSupportedModelName | TogetherAIModelName | GeminiModelName
+
+
+def get_model_info_from_name(model_name: str) -> ModelInfo:
+ if model_name == MY_MOCK_MODEL_INFO.model_name:
+ return MY_MOCK_MODEL_INFO
+ if model_name in (v for v in AnthropicModelName):
+ return ANTHROPIC_MODEL_INFO_BY_NAME[AnthropicModelName(model_name)]
+ elif model_name in (v for v in OpenAIModelName):
+ return get_openai_model_info(OpenAIModelName(model_name))
+ elif model_name in (v for v in GroqSupportedModelName):
+ return get_groq_model_info(GroqSupportedModelName(model_name))
+ elif model_name in (v for v in TogetherAIModelName):
+ return TOGETHERAI_MODEL_INFO_BY_NAME[TogetherAIModelName(model_name)]
+ elif model_name in (v for v in GeminiModelName):
+ return GEMINI_MODEL_INFO_BY_NAME[GeminiModelName(model_name)]
+ else:
+ raise Exception(f"Unknown model: {model_name}")
+
+
+def get_model_max_context_length(model_name: str) -> int:
+ model_info = get_model_info_from_name(model_name)
+ return model_info.max_input_tokens
+
+
+def get_model_max_output_tokens(model_name: str) -> int:
+ model_info = get_model_info_from_name(model_name)
+ if model_info.max_output_tokens is None:
+ raise ValueError(f"Model {model_name} does not have max_output_tokens defined")
+ return model_info.max_output_tokens
+
+
+def get_all_model_names() -> list[str]:
+ names = []
+ names.extend(list(v for v in AnthropicModelName))
+ names.extend(list(v for v in OpenAIModelName))
+ names.extend(list(v for v in GroqSupportedModelName))
+ names.extend(list(v for v in TogetherAIModelName))
+ names.extend(list(v for v in GeminiModelName))
+ return names
+
+
+def get_formatted_model_name(model_name: str) -> str:
+ """Get a nicely formatted model name.
+
+ Does things like removing generic prefixes like 'models/' and forward slashes (which can interfere with file names).
+
+ Some examples:
+
+ - `models/gemini-1.5-flash-001` -> `gemini-1.5-flash-001`
+ - 'groq/llama-3.3-70b-versatile' -> 'groq-llama-3.3-70b-versatile'
+ - 'claude-3-5-haiku-20241022' -> 'claude-3-5-haiku-20241022'
+ - 'together/google/gemma-2-27b-it' -> 'together-google-gemma-2-27b-it'
+
+ """
+ if model_name.startswith("models/"):
+ model_name = model_name[len("models/") :]
+ return model_name.replace("/", "-")
diff --git a/imbue_core/imbue_core/agents/llm_apis/constants.py b/vet/imbue_core/agents/llm_apis/constants.py
diff --git a/vet/imbue_core/agents/llm_apis/data_types.py b/vet/imbue_core/agents/llm_apis/data_types.py
@@ -0,0 +1,214 @@
+import datetime
+import enum
+import math
+from abc import ABC
+from typing import Any
+from typing import Generic
+from typing import TypeVar
+
+import attr
+from pydantic import ValidationInfo
+from pydantic import field_validator
+
+from vet.imbue_core.agents.llm_apis.union_data_types import ProviderSpecificCachingInfoUnion
+from vet.imbue_core.pydantic_serialization import SerializableModel
+from vet.imbue_core.serialization_types import Serializable
+from vet.imbue_core.time_utils import get_current_time
+
+__all__ = [
+ "CachedCostedLanguageModelResponse",
+ "ConversationMessage",
+ "CostedLanguageModelResponse",
+ "LanguageModelCompleteInputs",
+ "LanguageModelResponse",
+ "LanguageModelResponseUsage",
+ "LanguageModelResponseWithLogits",
+ "LanguageModelResponseWithThoughts",
+ "LanguageModelStreamInputs",
+ "ModelStr",
+ "ResponseStopReason",
+ "TokenProbability",
+]
+
+
+class ConversationMessage(SerializableModel):
+ role: str
+ content: str
+
+
+class ResponseStopReason(enum.StrEnum):
+ END_TURN = "end_turn"
+ MAX_TOKENS = "max_tokens"
+ STOP_SEQUENCE = "stop_sequence"
+ ERROR = "error"
+ NONE = "none"
+ # TODO: We aren't handling any of the below, we should likely error in these cases
+ CONTENT_FILTER = "content_filter"
+ TOOL_CALLS = "tool_calls"
+ FUNCTION_CALL = "function_call"
+
+ def response_not_finished(self) -> bool:
+ return self in {self.CONTENT_FILTER, self.MAX_TOKENS, self.ERROR}
+
+
+class TokenProbability(SerializableModel):
+ token: str
+ log_probability: float
+ is_stop: bool
+
+ @property
+ def probability(self) -> float:
+ return math.exp(self.log_probability)
+
+
+class ModelResponse(ABC):
+ pass
+
+
+class ThoughtResponse(SerializableModel, ModelResponse):
+ text: str
+ completion_tokens: int
+
+
+@attr.s(auto_attribs=True, frozen=True)
+class LanguageModelResponse(Serializable, ModelResponse):
+ text: str
+ token_count: int
+ stop_reason: ResponseStopReason
+ network_failure_count: int
+
+ def get_token_probability_sequence(
+ self,
+ ) -> tuple[tuple[TokenProbability, ...], ...] | None:
+ return None
+
+
+@attr.s(auto_attribs=True, frozen=True)
+class LanguageModelResponseWithThoughts(LanguageModelResponse):
+ thoughts: ThoughtResponse | None = None
+
+
+@attr.s(auto_attribs=True, frozen=True)
+class LanguageModelResponseWithLogits(LanguageModelResponse):
+ # guarantees that the first in each sequence was the one that was selected.
+ # the inner sequence are *not* guaranteed to be the same length, nor are they guaranteed to be sorted
+ token_probabilities: tuple[tuple[TokenProbability, ...], ...]
+
+ def get_token_probability_sequence(
+ self,
+ ) -> tuple[tuple[TokenProbability, ...], ...] | None:
+ return self.token_probabilities
+
+
+@attr.s(auto_attribs=True, frozen=True)
+class CountTokensResponse(Serializable, ModelResponse):
+ input_tokens: int
+ cached_content_token_count: int | None = None
+
+
+class CachingInfo(SerializableModel):
+ read_from_cache: int
+
+ # this should contain info that's not the same between providers. e.g. anthropic requires explicit cache writes with 5m or 1h duration,
+ # whereas openai does automatic prompt caching at no extra cost; so, we store cache write info here
+ provider_specific_data: ProviderSpecificCachingInfoUnion | None = None
+
+
+class LanguageModelResponseUsage(SerializableModel):
+ prompt_tokens_used: int
+ completion_tokens_used: int
+ dollars_used: float
+ caching_info: CachingInfo | None = None
+
+
+@attr.s(auto_attribs=True, frozen=True)
+class CostedLanguageModelResponse(Serializable, ModelResponse):
+ usage: LanguageModelResponseUsage
+ responses: tuple[LanguageModelResponse, ...]
+
+
+class ThinkConfig(SerializableModel):
+ # watch out: at least for gemini, this is a soft limit!
+ max_tokens: int | None = None
+ output_thinking: bool = False
+
+
+class LanguageModelGenerationParams(SerializableModel):
+ """Parameters for a single API call to an LLM. Excludes things that you don't want a default for, e.g. the prompt."""
+
+ temperature: float = 0.2
+ count: int = 1
+ max_tokens: int | None = None
+ stop: str | None = None
+ # specifically to allow generating new responses even when using caching
+ seed: int | None = None
+ thinking: ThinkConfig | None = None
+
+
+class ModelInputs(SerializableModel, ABC):
+ """Base class for inputs to an LLM API call."""
+
+
+class LanguageModelCompleteInputs(ModelInputs):
+ """Used to serialize the inputs for an LLM complete call."""
+
+ prompt: str
+ params: LanguageModelGenerationParams
+ network_failure_count: int
+
+
+class LanguageModelStreamInputs(ModelInputs):
+ """Used to serialize the inputs for an LLM stream call."""
+
+ prompt: str
+ params: LanguageModelGenerationParams
+
+
+class CountTokensInputs(ModelInputs):
+ """Used to serialize the inputs for a token count call."""
+
+ model: str
+ prompt: str
+
+
+InputsT = TypeVar("InputsT", bound=ModelInputs)
+ModelResponseT = TypeVar("ModelResponseT", bound=ModelResponse)
+
+
+@attr.s(auto_attribs=True, frozen=True)
+class CachedCostedModelResponse(Serializable, Generic[InputsT, ModelResponseT]):
+ response: ModelResponseT | None = None
+ error: str | None = None
+
+ # The timestamp is used to order cache entries when checking them in unit tests.
+ timestamp: datetime.datetime = attr.ib(factory=get_current_time)
+
+ # Cache entries are keyed based on an MD5 hash of the inputs to prevent the cache from growing too large.
+ # Here, we optionally store the inputs to the request.
+ # This is useful for unit tests to highlight changes in the prompt and other parts of the request.
+ # But since it can grow very large, we don't always store this information.
+ inputs: InputsT | None = None
+
+ @field_validator("response", "error")
+ def validate_response_or_error(cls, v: Any, info: ValidationInfo) -> Any:
+ if "response" in info.data and "error" in info.data:
+ if not ((info.data["response"] is None) ^ (info.data["error"] is None)):
+ raise ValueError("Must provide exactly one of response or error")
+ return v
+
+
+class CachedCostedLanguageModelResponse(
+ CachedCostedModelResponse[
+ LanguageModelCompleteInputs | LanguageModelStreamInputs,
+ CostedLanguageModelResponse,
+ ]
+):
+ pass
+
+
+class CachedCountTokensResponse(CachedCostedModelResponse[CountTokensInputs, CountTokensResponse]):
+ pass
+
+
+# to allow type checking to work when model names are passed as strings
+ModelStr = str
diff --git a/imbue_core/imbue_core/agents/llm_apis/errors.py b/vet/imbue_core/agents/llm_apis/errors.py
diff --git a/vet/imbue_core/agents/llm_apis/gemini_api.py b/vet/imbue_core/agents/llm_apis/gemini_api.py
@@ -0,0 +1,526 @@
+import enum
+import inspect
+from contextlib import contextmanager
+from pathlib import Path
+from types import FrameType
+from typing import AsyncGenerator
+from typing import Callable
+from typing import Final
+from typing import Iterable
+from typing import Iterator
+from typing import TypeVar
+
+import google.genai as genai
+import httpx
+from google.genai.errors import APIError
+from google.genai.types import BlockedReason
+from google.genai.types import ContentListUnion
+from google.genai.types import ContentUnion
+from google.genai.types import FinishReason
+from google.genai.types import GenerateContentConfig
+from google.genai.types import GenerateContentResponse
+from google.genai.types import HarmProbability
+from google.genai.types import ModelContent
+from google.genai.types import Part
+from google.genai.types import ThinkingConfig
+from google.genai.types import UserContent
+from loguru import logger
+from pydantic.functional_validators import field_validator
+
+from vet.imbue_core.agents.llm_apis.api_utils import convert_prompt_to_messages
+from vet.imbue_core.agents.llm_apis.api_utils import (
+ create_costed_language_model_response_for_single_result,
+)
+from vet.imbue_core.agents.llm_apis.data_types import CachedCountTokensResponse
+from vet.imbue_core.agents.llm_apis.data_types import CostedLanguageModelResponse
+from vet.imbue_core.agents.llm_apis.data_types import CountTokensInputs
+from vet.imbue_core.agents.llm_apis.data_types import CountTokensResponse
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelGenerationParams
+from vet.imbue_core.agents.llm_apis.data_types import ResponseStopReason
+from vet.imbue_core.agents.llm_apis.data_types import ThoughtResponse
+from vet.imbue_core.agents.llm_apis.errors import BadAPIRequestError
+from vet.imbue_core.agents.llm_apis.errors import LanguageModelInvalidModelNameError
+from vet.imbue_core.agents.llm_apis.errors import MissingAPIKeyError
+from vet.imbue_core.agents.llm_apis.errors import TransientLanguageModelError
+from vet.imbue_core.agents.llm_apis.errors import UnsetCachePathError
+from vet.imbue_core.agents.llm_apis.language_model_api import LanguageModelAPI
+from vet.imbue_core.agents.llm_apis.models import ModelInfo
+from vet.imbue_core.agents.llm_apis.stream import LanguageModelStreamEvent
+from vet.imbue_core.async_monkey_patches import log_exception
+from vet.imbue_core.caching import AsyncCache
+from vet.imbue_core.frozen_utils import FrozenDict
+from vet.imbue_core.frozen_utils import FrozenMapping
+from vet.imbue_core.itertools import only
+from vet.imbue_core.secrets_utils import get_secret
+
+
+class GeminiModelName(enum.StrEnum):
+ GEMINI_1_0_PRO = "models/gemini-1.0-pro-001"
+ GEMINI_1_5_FLASH = "models/gemini-1.5-flash-001"
+ GEMINI_1_5_PRO = "models/gemini-1.5-pro-001"
+ GEMINI_1_5_PRO_2 = "models/gemini-1.5-pro-002"
+ GEMINI_1_5_FLASH_2 = "models/gemini-1.5-flash-002"
+ GEMINI_2_0_FLASH = "models/gemini-2.0-flash-001"
+ GEMINI_2_5_FLASH = "models/gemini-2.5-flash"
+ GEMINI_2_5_FLASH_LITE_PREVIEW = "models/gemini-2.5-flash-lite-preview-06-17"
+
+
+# Rate limits for Google Gemini models based on published API documentation
+# Reference: https://ai.google.dev/gemini-api/docs/rate-limits#tier-3
+# Using Tier 3 rate limits
+
+GEMINI_MODEL_INFO_BY_NAME: FrozenMapping[GeminiModelName, ModelInfo] = FrozenDict(
+ {
+ # https://ai.google.dev/gemini-api/docs/models/gemini
+ # https://ai.google.dev/pricing
+ # For pricing there are different rates depending on context/prompt size, so below we use the most
+ # expensive value. Note that this only kicks in at 128k tokens, the cost for most prompts is 2x lower
+ GeminiModelName.GEMINI_1_0_PRO: ModelInfo(
+ model_name="models/gemini-1.0-pro-001",
+ cost_per_input_token=0.5 / 1_000_000,
+ cost_per_output_token=1.5 / 1_000_000,
+ max_input_tokens=30_720,
+ max_output_tokens=2048,
+ rate_limit_req=2000 / 60, # 2000 RPM = 33.33 RPS
+ ),
+ GeminiModelName.GEMINI_1_5_FLASH: ModelInfo(
+ model_name="models/gemini-1.5-flash-001",
+ cost_per_input_token=0.15 / 1_000_000,
+ cost_per_output_token=0.60 / 1_000_000,
+ max_input_tokens=1_048_576,
+ max_output_tokens=8192,
+ rate_limit_req=30000 / 60, # 30000 RPM = 500.00 RPS
+ ),
+ GeminiModelName.GEMINI_1_5_FLASH_2: ModelInfo(
+ model_name="models/gemini-1.5-flash-002",
+ cost_per_input_token=0.15 / 1_000_000,
+ cost_per_output_token=0.60 / 1_000_000,
+ max_input_tokens=1_048_576,
+ max_output_tokens=8192,
+ rate_limit_req=30000 / 60, # 30000 RPM = 500.00 RPS
+ ),
+ GeminiModelName.GEMINI_1_5_PRO: ModelInfo(
+ model_name="models/gemini-1.5-pro-001",
+ cost_per_input_token=2.5 / 1_000_000,
+ cost_per_output_token=10.0 / 1_000_000,
+ max_input_tokens=2_097_152,
+ max_output_tokens=8192,
+ rate_limit_req=4000 / 60, # 4000 RPM = 66.67 RPS
+ ),
+ GeminiModelName.GEMINI_1_5_PRO_2: ModelInfo(
+ model_name="models/gemini-1.5-pro-002",
+ cost_per_input_token=2.5 / 1_000_000,
+ cost_per_output_token=10.0 / 1_000_000,
+ max_input_tokens=2_097_152,
+ max_output_tokens=8192,
+ rate_limit_req=4000 / 60, # 4000 RPM = 66.67 RPS
+ ),
+ GeminiModelName.GEMINI_2_0_FLASH: ModelInfo(
+ model_name="models/gemini-2.0-flash-001",
+ cost_per_input_token=0.1 / 1_000_000,
+ cost_per_output_token=0.4 / 1_000_000,
+ max_input_tokens=1_048_576,
+ max_output_tokens=8192,
+ rate_limit_req=30000 / 60, # 30000 RPM = 500.00 RPS
+ ),
+ GeminiModelName.GEMINI_2_5_FLASH: ModelInfo(
+ model_name="models/gemini-2.5-flash",
+ cost_per_input_token=0.3 / 1_000_000,
+ cost_per_output_token=2.5 / 1_000_000,
+ max_input_tokens=1_048_576,
+ max_output_tokens=65536,
+ rate_limit_req=10_000 / 60, # 10000 RPM = 166.67 RPS
+ rate_limit_tok=8_000_000 / 60, # 8,000,000 TPM = 133,333.33 TPS
+ max_thinking_budget=24576,
+ ),
+ GeminiModelName.GEMINI_2_5_FLASH_LITE_PREVIEW: ModelInfo(
+ model_name="models/gemini-2.5-flash-lite-preview-06-17",
+ cost_per_input_token=0.1 / 1_000_000,
+ cost_per_output_token=0.4 / 1_000_000,
+ max_input_tokens=1_000_000,
+ max_output_tokens=64_000,
+ # these are the tier 2 rate limits. the above claims that we're on tier 3, but i've never actually seen that
+ rate_limit_req=10_000 / 60,
+ rate_limit_tok=10_000_000 / 60,
+ # rate_limit_req=30_000 / 60, # 30000 RPM = 500.00 RPS
+ # rate_limit_tok=30_000_000 / 60, # 30,000,000 TPM = 500,000 TPS
+ max_thinking_budget=24_576,
+ ),
+ }
+)
+
+
+_ROLE_TO_GEMINI_ROLE: Final[FrozenMapping[str, str]] = FrozenDict(
+ {
+ "HUMAN": "user",
+ "ASSISTANT": "model",
+ "USER": "user",
+ "SYSTEM": "user",
+ }
+)
+
+NO_SIMPLE_TEXT_ERROR = "".join(
+ [
+ "The `response.text` quick accessor only works for ",
+ "simple (single-`Part`) text responses. This response is not simple text.",
+ "Use the `result.parts` accessor or the full ",
+ "`result.candidates[index].content.parts` lookup ",
+ "instead.",
+ ]
+)
+
+_GEMINI_STOP_REASON_TO_STOP_REASON: Final[FrozenMapping[FinishReason, ResponseStopReason]] = FrozenDict(
+ {
+ # Gemini treats stop due to natural stop point and provided stop sequence the same
+ FinishReason.STOP: ResponseStopReason.END_TURN,
+ FinishReason.MAX_TOKENS: ResponseStopReason.MAX_TOKENS,
+ FinishReason.SAFETY: ResponseStopReason.CONTENT_FILTER,
+ # Recitation means the content was flagged for being memorized, i.e. the LLM just
+ # copied data from the training data (@johnny at least that's how I understood the docs)
+ # https://ai.google.dev/api/generate-content#FinishReason
+ FinishReason.RECITATION: ResponseStopReason.CONTENT_FILTER,
+ FinishReason.OTHER: ResponseStopReason.NONE,
+ FinishReason.FINISH_REASON_UNSPECIFIED: ResponseStopReason.NONE,
+ }
+)
+
+T = TypeVar("T")
+
+
+def only_and_not_none(iterable: Iterable[T] | None) -> T:
+ in_value = iterable if iterable is not None else []
+ return only(in_value)
+
+
+def _is_flagged_as_unsafe(api_result: GenerateContentResponse) -> bool:
+ if api_result.prompt_feedback is None:
+ return False
+ block_reason = api_result.prompt_feedback.block_reason
+ if block_reason == BlockedReason.SAFETY:
+ return True
+ candidate = only_and_not_none(api_result.candidates)
+ if candidate.finish_reason == FinishReason.SAFETY:
+ return True
+ if candidate.finish_reason == FinishReason.OTHER and any(
+ rating.probability != HarmProbability.NEGLIGIBLE for rating in (candidate.safety_ratings or [])
+ ):
+ return True
+ return False
+
+
+def _is_flagged_as_recitation(api_result: GenerateContentResponse) -> bool:
+ candidate = only_and_not_none(api_result.candidates)
+ finish_reason = candidate.finish_reason
+ if finish_reason == FinishReason.RECITATION:
+ return True
+ return False
+
+
+def role_to_content(role: str, parts: list[Part]) -> ContentUnion:
+ match role:
+ case "user":
+ return UserContent(parts=parts)
+ case "model":
+ return ModelContent(parts=parts)
+ case _:
+ raise BadAPIRequestError(f"Invalid role: {role}")
+
+
+def convert_prompt_to_gemini_messages(prompt: str) -> ContentListUnion:
+ messages: list[ContentUnion] = []
+ parts = []
+ last_role = None
+ for message in convert_prompt_to_messages(prompt):
+ role = _ROLE_TO_GEMINI_ROLE[message.role]
+ parts.append(Part(text=f"\n{message.content}"))
+ if last_role != role and last_role is not None:
+ messages.append(role_to_content(last_role, parts))
+ parts = []
+ last_role = role
+ if len(parts) > 0:
+ assert last_role is not None
+ messages.append(role_to_content(last_role, parts))
+ return messages
+
+
+@contextmanager
+def _gemini_exception_manager() -> Iterator[None]:
+ """Simple context manager for parsing gemini API exceptions."""
+ # TODO probably some exceptions missing here. The google.ai docs/code is annoying to parse
+ try:
+ yield
+ except AssertionError as e:
+ logger.info("The Gemini prompt is invalid.")
+ raise BadAPIRequestError(str(e)) from e
+ except APIError as e:
+ logger.info("Gemini failed to generate content.")
+ raise BadAPIRequestError(str(e)) from e
+ except ValueError as e:
+ logger.info("Gemini did not return a simple text response.")
+ raise BadAPIRequestError(str(e)) from e
+ except AttributeError as e:
+ logger.info("There is an error with the Gemini prompt or processing code: {}.", str(e))
+ raise BadAPIRequestError(str(e)) from e
+ except httpx.RemoteProtocolError as e:
+ logger.info(str(e))
+ raise TransientLanguageModelError("httpx.RemoteProtocolError") from e
+ except (BadAPIRequestError, TransientLanguageModelError, MissingAPIKeyError):
+ # we already raised this error ourselves earlier, so we don't need to mark it as unknown
+ raise
+ except Exception as e:
+ # we catch TransientLanguageModelError later to retry it, but we still want to log it so it's not silent
+ log_exception(
+ e,
+ "Failed to generate output from Gemini, unknown error of type {type_name}",
+ type_name=type(e).__name__,
+ )
+ raise TransientLanguageModelError("Unknown error") from e
+
+
+R = TypeVar("R")
+
+
+def fmap(fn: Callable[[T], R], values: T | None) -> R | None:
+ if values is None:
+ return None
+ return fn(values)
+
+
+class GeminiAPI(LanguageModelAPI):
+ model_name: GeminiModelName = GeminiModelName.GEMINI_1_5_FLASH
+ is_conversational: bool = True
+
+ count_tokens_cache_path: Path | None = None
+
+ @field_validator("model_name") # pyre-ignore[56]: pyre doesn't understand pydantic
+ @classmethod
+ def validate_model_name(cls, v: str) -> str:
+ if v not in GEMINI_MODEL_INFO_BY_NAME:
+ raise LanguageModelInvalidModelNameError(v, cls.__name__, list(GEMINI_MODEL_INFO_BY_NAME))
+ return v
+
+ @property
+ def model_info(self) -> ModelInfo:
+ return GEMINI_MODEL_INFO_BY_NAME[self.model_name]
+
+ def _get_client(self) -> genai.Client:
+ api_key = get_secret("GOOGLE_API_KEY")
+ if not api_key:
+ raise MissingAPIKeyError("GOOGLE_API_KEY environment variable is not set")
+ return genai.Client(api_key=api_key)
+
+ async def _call_api(
+ self,
+ prompt: str,
+ params: LanguageModelGenerationParams,
+ network_failure_count: int = 0,
+ ) -> CostedLanguageModelResponse:
+ # TODO: check if this is still true
+ assert params.count == 1, "Gemini only supports a single completion"
+ messages = convert_prompt_to_gemini_messages(prompt)
+ with _gemini_exception_manager():
+ client = self._get_client()
+ generation_config = GenerateContentConfig(
+ temperature=params.temperature,
+ candidate_count=params.count,
+ stop_sequences=fmap(lambda x: [x], params.stop),
+ max_output_tokens=params.max_tokens,
+ thinking_config=fmap(
+ lambda thinking: ThinkingConfig(
+ thinking_budget=thinking.max_tokens,
+ include_thoughts=thinking.output_thinking,
+ ),
+ params.thinking,
+ ),
+ )
+
+ api_result: GenerateContentResponse = await client.aio.models.generate_content(
+ model=self.model_info.model_name,
+ contents=messages,
+ config=generation_config,
+ )
+
+ prompt_tokens = self.count_tokens(prompt)
+
+ if (
+ api_result.prompt_feedback is not None
+ and api_result.prompt_feedback.block_reason is not None
+ and api_result.prompt_feedback.block_reason != BlockedReason.BLOCKED_REASON_UNSPECIFIED
+ ):
+ logger.info(
+ f"Gemini blocked output: {messages=}, {api_result.prompt_feedback.block_reason=}, {api_result.prompt_feedback.safety_ratings=}"
+ )
+ return create_costed_language_model_response_for_single_result(
+ text="",
+ prompt_tokens=prompt_tokens,
+ completion_tokens=0,
+ stop_reason=ResponseStopReason.NONE,
+ network_failure_count=network_failure_count,
+ dollars_used=self.calculate_cost(prompt_tokens, 0), # guestimate of cost,
+ )
+
+ if _is_flagged_as_unsafe(api_result) or _is_flagged_as_recitation(api_result):
+ block_reason = fmap(lambda x: x.block_reason, api_result.prompt_feedback)
+ safety_ratings = (
+ api_result.prompt_feedback.safety_ratings if api_result.prompt_feedback is not None else None
+ )
+ logger.info(
+ "Gemini flagged output: block_reason={block_reason}, safety_ratings={safety_ratings}",
+ block_reason=block_reason,
+ safety_ratings=safety_ratings,
+ )
+ return create_costed_language_model_response_for_single_result(
+ text="",
+ prompt_tokens=prompt_tokens,
+ completion_tokens=0,
+ stop_reason=ResponseStopReason.CONTENT_FILTER,
+ network_failure_count=network_failure_count,
+ dollars_used=self.calculate_cost(prompt_tokens, 0),
+ )
+
+ candidate = only_and_not_none(api_result.candidates)
+ finish_reason = candidate.finish_reason
+ parsed_finish_reason = (
+ _GEMINI_STOP_REASON_TO_STOP_REASON[finish_reason]
+ if finish_reason is not None
+ else ResponseStopReason.NONE
+ )
+
+ if finish_reason not in [FinishReason.MAX_TOKENS, FinishReason.STOP]:
+ block_reason = fmap(lambda x: x.block_reason, api_result.prompt_feedback)
+ safety_ratings = fmap(lambda x: x.safety_ratings, api_result.prompt_feedback)
+ logger.info(
+ f"Gemini did not return a simple text response, {block_reason=}, {safety_ratings=}, {finish_reason=}, {candidate.safety_ratings=}"
+ )
+ return create_costed_language_model_response_for_single_result(
+ text="",
+ prompt_tokens=prompt_tokens,
+ completion_tokens=0,
+ stop_reason=parsed_finish_reason,
+ network_failure_count=network_failure_count,
+ dollars_used=self.calculate_cost(prompt_tokens, 0),
+ )
+
+ text = api_result.text
+
+ thoughts_list = fmap(
+ lambda content: fmap(
+ lambda parts: [part.text for part in parts if part.thought],
+ content.parts,
+ ),
+ candidate.content,
+ )
+ if not thoughts_list:
+ thoughts = None
+ else:
+ thoughts = only(thoughts_list)
+
+ if text is None:
+ if finish_reason == FinishReason.MAX_TOKENS and generation_config.thinking_config is not None:
+ raise BadAPIRequestError(
+ "Gemini ran out of tokens while thinking and did not return a text response"
+ )
+ logger.info("Non-simple-text response: {}", api_result)
+ raise BadAPIRequestError("Gemini did not return a simple text response (text is None)")
+
+ prompt_tokens = (
+ api_result.usage_metadata.prompt_token_count
+ if api_result.usage_metadata is not None and api_result.usage_metadata.prompt_token_count is not None
+ else self.count_tokens(prompt)
+ )
+
+ thought_tokens = (
+ api_result.usage_metadata.thoughts_token_count
+ if api_result.usage_metadata is not None and api_result.usage_metadata.thoughts_token_count is not None
+ else 0
+ )
+
+ output_tokens = (
+ api_result.usage_metadata.candidates_token_count
+ if api_result.usage_metadata is not None
+ and api_result.usage_metadata.candidates_token_count is not None
+ else self.count_tokens(text)
+ )
+
+ completion_tokens = output_tokens + thought_tokens
+
+ dollars_used = self.calculate_cost(prompt_tokens, completion_tokens)
+ logger.trace(text)
+ logger.trace("Dollars used: {}", dollars_used)
+ return create_costed_language_model_response_for_single_result(
+ text=text,
+ prompt_tokens=prompt_tokens,
+ completion_tokens=completion_tokens,
+ stop_reason=parsed_finish_reason,
+ network_failure_count=network_failure_count,
+ dollars_used=dollars_used,
+ thoughts=fmap(
+ lambda x: ThoughtResponse(text=x, completion_tokens=thought_tokens),
+ thoughts,
+ ),
+ )
+
+ def _get_api_stream(
+ self,
+ prompt: str,
+ params: LanguageModelGenerationParams,
+ ) -> AsyncGenerator[LanguageModelStreamEvent, None]:
+ # TODO Implement streaming support (?)
+ raise NotImplementedError()
+
+ # TODO: these are the same as in anthropic_api.py. it might be good to refactor so that both AnthropicAPI and GeminiAPI inherit from a class LanguageModelAPIWithCountTokens which has these methods
+ def get_count_tokens_response_cache(self) -> AsyncCache[CachedCountTokensResponse]:
+ if self.count_tokens_cache_path is None:
+ raise UnsetCachePathError()
+ return AsyncCache(self.count_tokens_cache_path, CachedCountTokensResponse)
+
+ async def check_count_tokens_cache(self, cache_key: str) -> CountTokensResponse | None:
+ return await self.check_cache_core(self.get_count_tokens_response_cache, cache_key)
+
+ async def _get_from_count_tokens_cache(
+ self, frame: FrameType | None
+ ) -> tuple[str | None, CountTokensResponse | None]:
+ return await self._get_from_cache_core(frame, lambda cr: cr, self.check_count_tokens_cache)
+
+ async def count_tokens_api(self, text: str, is_caching_enabled: bool) -> int | None:
+ """Call the count_tokens api to get a definitive token count. May be fragile and is definitely slow."""
+
+ self.assert_caching_enabled_if_offline(is_caching_enabled)
+
+ frame: FrameType | None = None
+ if is_caching_enabled:
+ frame = inspect.currentframe()
+
+ cache_key: str | None = None
+ if is_caching_enabled:
+ cache_key, cached_response = await self._get_from_count_tokens_cache(frame)
+
+ if cached_response is not None:
+ return cached_response.input_tokens
+
+ self.assert_not_offline_if_cache_miss(text)
+
+ with _gemini_exception_manager():
+ client = self._get_client()
+ response = client.models.count_tokens(model=self.model_info.model_name, contents=text)
+
+ total_tokens = response.total_tokens
+ if total_tokens is None:
+ raise TransientLanguageModelError("Gemini did not return a valid token count")
+
+ result = CachedCountTokensResponse(
+ response=CountTokensResponse(
+ input_tokens=total_tokens,
+ cached_content_token_count=response.cached_content_token_count,
+ ),
+ inputs=(
+ CountTokensInputs(model=self.model_info.model_name, prompt=text) if self.is_caching_inputs else None
+ ),
+ )
+
+ if is_caching_enabled:
+ assert cache_key is not None
+ async with self.get_count_tokens_response_cache() as cache:
+ await cache.set(cache_key, result)
+
+ return total_tokens
diff --git a/vet/imbue_core/agents/llm_apis/groq_api.py b/vet/imbue_core/agents/llm_apis/groq_api.py
@@ -0,0 +1,357 @@
+import asyncio
+import enum
+import math
+from contextlib import contextmanager
+from typing import AsyncGenerator
+from typing import Final
+from typing import Iterator
+from typing import Mapping
+
+import httpx
+from groq import APIConnectionError
+from groq import APIError
+from groq import AsyncGroq
+from groq import AsyncStream
+from groq import BadRequestError
+from groq import RateLimitError
+from groq.types.chat import ChatCompletion
+from loguru import logger
+from pydantic.functional_validators import field_validator
+
+from vet.imbue_core.agents.llm_apis.api_utils import convert_prompt_to_openai_messages
+from vet.imbue_core.agents.llm_apis.data_types import CostedLanguageModelResponse
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelGenerationParams
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelResponse
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelResponseUsage
+from vet.imbue_core.agents.llm_apis.data_types import ResponseStopReason
+from vet.imbue_core.agents.llm_apis.errors import BadAPIRequestError
+from vet.imbue_core.agents.llm_apis.errors import LanguageModelInvalidModelNameError
+from vet.imbue_core.agents.llm_apis.errors import MissingAPIKeyError
+from vet.imbue_core.agents.llm_apis.errors import PromptTooLongError
+from vet.imbue_core.agents.llm_apis.errors import TransientLanguageModelError
+from vet.imbue_core.agents.llm_apis.language_model_api import LanguageModelAPI
+from vet.imbue_core.agents.llm_apis.models import ModelInfo
+from vet.imbue_core.agents.llm_apis.stream import LanguageModelStreamDeltaEvent
+from vet.imbue_core.agents.llm_apis.stream import LanguageModelStreamEndEvent
+from vet.imbue_core.agents.llm_apis.stream import LanguageModelStreamEvent
+from vet.imbue_core.agents.llm_apis.stream import LanguageModelStreamStartEvent
+from vet.imbue_core.frozen_utils import FrozenDict
+from vet.imbue_core.frozen_utils import FrozenMapping
+from vet.imbue_core.itertools import only
+from vet.imbue_core.secrets_utils import get_secret
+
+# note: we require that these model versions are explicit, just like the rest of our dependencies
+# the reason is that these models are actually now mostly deterministic, and it is much easier to debug if we know what model was used
+# also, there's no need to troll yourself by wondering why results have improved (or gotten worse) when you dont realized that the version has shifted under you
+# if you want to use an upgraded model, just upgrade the model to the key displayed on the website
+# please do NOT set these back to the generic model names!
+
+
+# TODO: there are likely more models to add
+class GroqSupportedModelName(enum.StrEnum):
+ GROQ_GEMMA2_9B_IT = "groq/gemma2-9b-it"
+ GROQ_LLAMA3_70B_8192 = "groq/llama3-70b-8192"
+ GROQ_LLAMA3_8B_8192 = "groq/llama3-8b-8192"
+ GROQ_LLAMA_3_3_70B_SPECDEC = "groq/llama-3.3-70b-specdec"
+ GROQ_MIXTRAL_8X7B_32768 = "groq/mixtral-8x7b-32768"
+ GROQ_LLAMA_3_3_70B_VERSATILE = "groq/llama-3.3-70b-versatile"
+ GROQ_LLAMA_3_1_8B_INSTANT = "groq/llama-3.1-8b-instant"
+ GROQ_LLAMA_3_2_1B_PREVIEW = "groq/llama-3.2-1b-preview"
+ GROQ_LLAMA_3_2_3B_PREVIEW = "groq/llama-3.2-3b-preview"
+
+
+# Rate limits for Groq models based on custom rate limits for our organization.
+# See here https://console.groq.com/dashboard/limits (requires login, use your Google account)
+
+GROQ_MODEL_INFO_BY_NAME: FrozenMapping[GroqSupportedModelName, ModelInfo] = FrozenDict(
+ {
+ GroqSupportedModelName.GROQ_GEMMA2_9B_IT: ModelInfo(
+ model_name=str(GroqSupportedModelName.GROQ_GEMMA2_9B_IT),
+ cost_per_input_token=0.20 / 1_000_000,
+ cost_per_output_token=0.20 / 1_000_000,
+ max_input_tokens=8192,
+ max_output_tokens=None,
+ rate_limit_req=30 / 60, # 30 RPM = 0.50 RPS
+ ),
+ GroqSupportedModelName.GROQ_LLAMA3_70B_8192: ModelInfo(
+ model_name=str(GroqSupportedModelName.GROQ_LLAMA3_70B_8192),
+ cost_per_input_token=0.59 / 1_000_000,
+ cost_per_output_token=0.79 / 1_000_000,
+ max_input_tokens=8192,
+ max_output_tokens=None,
+ rate_limit_req=30 / 60, # 30 RPM = 0.50 RPS
+ ),
+ GroqSupportedModelName.GROQ_LLAMA3_8B_8192: ModelInfo(
+ model_name=str(GroqSupportedModelName.GROQ_LLAMA3_8B_8192),
+ cost_per_input_token=0.05 / 1_000_000,
+ cost_per_output_token=0.08 / 1_000_000,
+ max_input_tokens=8192,
+ max_output_tokens=None,
+ rate_limit_req=30 / 60, # 30 RPM = 0.50 RPS
+ ),
+ GroqSupportedModelName.GROQ_LLAMA_3_3_70B_SPECDEC: ModelInfo(
+ model_name=str(GroqSupportedModelName.GROQ_LLAMA_3_3_70B_SPECDEC),
+ cost_per_input_token=0.59 / 1_000_000,
+ cost_per_output_token=0.99 / 1_000_000,
+ max_input_tokens=8192,
+ max_output_tokens=None,
+ rate_limit_req=30 / 60, # 30 RPM = 0.50 RPS
+ ),
+ GroqSupportedModelName.GROQ_MIXTRAL_8X7B_32768: ModelInfo(
+ model_name=str(GroqSupportedModelName.GROQ_MIXTRAL_8X7B_32768),
+ cost_per_input_token=0.24 / 1_000_000,
+ cost_per_output_token=0.24 / 1_000_000,
+ max_input_tokens=32768,
+ max_output_tokens=None,
+ rate_limit_req=30 / 60, # 30 RPM = 0.50 RPS
+ ),
+ GroqSupportedModelName.GROQ_LLAMA_3_3_70B_VERSATILE: ModelInfo(
+ model_name=str(GroqSupportedModelName.GROQ_LLAMA_3_3_70B_VERSATILE),
+ cost_per_input_token=0.59 / 1_000_000,
+ cost_per_output_token=0.79 / 1_000_000,
+ max_input_tokens=128_000,
+ max_output_tokens=None,
+ rate_limit_req=30 / 60, # 30 RPM = 0.50 RPS
+ ),
+ GroqSupportedModelName.GROQ_LLAMA_3_1_8B_INSTANT: ModelInfo(
+ model_name=str(GroqSupportedModelName.GROQ_LLAMA_3_1_8B_INSTANT),
+ cost_per_input_token=0.05 / 1_000_000,
+ cost_per_output_token=0.08 / 1_000_000,
+ max_input_tokens=128_000,
+ max_output_tokens=None,
+ rate_limit_req=30 / 60, # 30 RPM = 0.50 RPS
+ ),
+ GroqSupportedModelName.GROQ_LLAMA_3_2_1B_PREVIEW: ModelInfo(
+ model_name=str(GroqSupportedModelName.GROQ_LLAMA_3_2_1B_PREVIEW),
+ cost_per_input_token=0.04 / 1_000_000,
+ cost_per_output_token=0.04 / 1_000_000,
+ max_input_tokens=128_000,
+ max_output_tokens=None,
+ rate_limit_req=30 / 60, # 30 RPM = 0.50 RPS
+ ),
+ GroqSupportedModelName.GROQ_LLAMA_3_2_3B_PREVIEW: ModelInfo(
+ model_name=str(GroqSupportedModelName.GROQ_LLAMA_3_2_3B_PREVIEW),
+ cost_per_input_token=0.06 / 1_000_000,
+ cost_per_output_token=0.06 / 1_000_000,
+ max_input_tokens=128_000,
+ max_output_tokens=None,
+ rate_limit_req=30 / 60, # 30 RPM = 0.50 RPS
+ ),
+ }
+)
+
+
+def get_model_info(model_name: GroqSupportedModelName) -> ModelInfo:
+ return GROQ_MODEL_INFO_BY_NAME[model_name]
+
+
+_CAPACITY_SEMAPHOR_BY_MODEL_NAME: Mapping[str, asyncio.Semaphore] = {
+ GroqSupportedModelName.GROQ_GEMMA2_9B_IT: asyncio.Semaphore(100),
+ GroqSupportedModelName.GROQ_LLAMA3_70B_8192: asyncio.Semaphore(100),
+ GroqSupportedModelName.GROQ_LLAMA3_8B_8192: asyncio.Semaphore(100),
+ GroqSupportedModelName.GROQ_LLAMA_3_3_70B_SPECDEC: asyncio.Semaphore(100),
+ GroqSupportedModelName.GROQ_MIXTRAL_8X7B_32768: asyncio.Semaphore(100),
+ GroqSupportedModelName.GROQ_LLAMA_3_3_70B_VERSATILE: asyncio.Semaphore(100),
+ GroqSupportedModelName.GROQ_LLAMA_3_1_8B_INSTANT: asyncio.Semaphore(100),
+ GroqSupportedModelName.GROQ_LLAMA_3_2_1B_PREVIEW: asyncio.Semaphore(100),
+ GroqSupportedModelName.GROQ_LLAMA_3_2_3B_PREVIEW: asyncio.Semaphore(100),
+}
+
+
+def _get_capacity_semaphor(model_name: str) -> asyncio.Semaphore:
+ return _CAPACITY_SEMAPHOR_BY_MODEL_NAME[model_name]
+
+
+# ref: https://github.com/groq/groq-python/blob/b74ce9e301115520c744e18425653a4c783cb6f5/src/groq/types/chat/chat_completion_chunk.py#L86
+_GROQ_STOP_REASON_TO_STOP_REASON: Final[FrozenMapping[str, ResponseStopReason]] = FrozenDict(
+ {
+ # Groq copies OpenAI and treats stop due to natural stop point and provided stop sequence the same
+ "stop": ResponseStopReason.END_TURN,
+ "length": ResponseStopReason.MAX_TOKENS,
+ "tool_calls": ResponseStopReason.TOOL_CALLS,
+ "function_call": ResponseStopReason.FUNCTION_CALL,
+ "content_filter": ResponseStopReason.CONTENT_FILTER,
+ }
+)
+
+
+@contextmanager
+def _groq_exception_manager() -> Iterator[None]:
+ """Simple context manager for parsing groq exceptions mostly based on how we parse OpenAI API exceptions."""
+ try:
+ yield
+ except BadRequestError as e:
+ logger.info("BadAPIRequestError {}", e)
+ raise BadAPIRequestError(str(e)) from e
+ except APIConnectionError as e:
+ logger.info("Rate limited? Received APIConnectionError {}", e)
+ raise TransientLanguageModelError("APIConnectionError") from e
+ except RateLimitError as e:
+ logger.info("Rate limited? {}", e)
+ raise TransientLanguageModelError("RateLimitError") from e
+ except httpx.RemoteProtocolError as e:
+ logger.info("{}", e)
+ raise TransientLanguageModelError("httpx.RemoteProtocolError") from e
+ except APIError as e:
+ if e.body["code"] == "context_length_exceeded": # type: ignore
+ # TODO: eventually fix elsewhere, since this doesn't actually give you any information in the body...
+ raise PromptTooLongError(prompt_len=1, max_prompt_len=1)
+ raise TransientLanguageModelError("APIError") from e
+
+
+class GroqChatAPI(LanguageModelAPI):
+ model_name: GroqSupportedModelName = GroqSupportedModelName.GROQ_LLAMA3_8B_8192
+ is_conversational: bool = True
+ presence_penalty: float = 0.0
+ # this shouldn't really ever even be used, but just in case
+ stop_token_log_probability: float = math.log(0.9999)
+
+ @field_validator("model_name") # pyre-ignore[56]: pyre doesn't understand pydantic
+ @classmethod
+ def validate_model_name(cls, v: str) -> str:
+ if v not in GROQ_MODEL_INFO_BY_NAME:
+ raise LanguageModelInvalidModelNameError(v, cls.__name__, list(GROQ_MODEL_INFO_BY_NAME))
+ return v
+
+ @property
+ def model_info(self) -> ModelInfo:
+ return GROQ_MODEL_INFO_BY_NAME[self.model_name]
+
+ @property
+ def external_model_name(self) -> str:
+ return self.model_name.replace("groq/", "")
+
+ def _get_client(self) -> AsyncGroq:
+ api_key = get_secret("GROQ_API_KEY")
+ if not api_key:
+ raise MissingAPIKeyError("GROQ_API_KEY environment variable is not set")
+ return AsyncGroq(api_key=api_key)
+
+ async def _call_api(
+ self,
+ prompt: str,
+ params: LanguageModelGenerationParams,
+ network_failure_count: int = 0,
+ ) -> CostedLanguageModelResponse:
+ with _groq_exception_manager():
+ messages = convert_prompt_to_openai_messages(prompt)
+ client = self._get_client()
+ async with _get_capacity_semaphor(self.model_name):
+ # logger.info("Open requests: {}", semaphor._value)
+ api_result = await client.chat.completions.create(
+ model=self.external_model_name,
+ messages=messages, # type: ignore
+ max_tokens=params.max_tokens,
+ n=params.count,
+ temperature=params.temperature,
+ stop=params.stop,
+ logprobs=False,
+ seed=params.seed,
+ stream=False,
+ presence_penalty=self.presence_penalty,
+ )
+ assert isinstance(api_result, ChatCompletion)
+
+ results = []
+ for data in api_result.choices:
+ assert data.message.content is not None
+
+ assert data.logprobs is not None and data.logprobs.content is not None
+ text = data.message.content
+
+ stop_reason = _GROQ_STOP_REASON_TO_STOP_REASON[str(data.finish_reason)]
+
+ # Note, like OpenAI, Groq treats end turn and stop sequence the same
+ # Here we assume it is stop sequence if user has specified a stop sequence
+ if params.stop is not None and stop_reason == ResponseStopReason.END_TURN:
+ text += params.stop
+ result = LanguageModelResponse(
+ text=text,
+ token_count=0,
+ stop_reason=stop_reason,
+ network_failure_count=network_failure_count,
+ )
+ results.append(result)
+
+ logger.trace("text: " + results[0].text)
+ if api_result.usage is not None:
+ completion_tokens = api_result.usage.completion_tokens
+ prompt_tokens = api_result.usage.prompt_tokens
+ else:
+ completion_tokens = 0
+ prompt_tokens = 0
+ dollars_used = self.calculate_cost(prompt_tokens, completion_tokens)
+ logger.trace("dollars used: {}", dollars_used)
+ return CostedLanguageModelResponse(
+ usage=LanguageModelResponseUsage(
+ prompt_tokens_used=prompt_tokens,
+ completion_tokens_used=completion_tokens,
+ dollars_used=dollars_used,
+ ),
+ responses=tuple(results),
+ )
+
+ async def _get_api_stream(
+ self,
+ prompt: str,
+ params: LanguageModelGenerationParams,
+ ) -> AsyncGenerator[LanguageModelStreamEvent, None]:
+ with _groq_exception_manager():
+ messages = convert_prompt_to_openai_messages(prompt)
+ client = self._get_client()
+ async with _get_capacity_semaphor(self.model_name):
+ api_result = await client.chat.completions.create(
+ model=self.external_model_name,
+ messages=messages, # type: ignore
+ max_tokens=params.max_tokens,
+ n=1,
+ temperature=params.temperature,
+ stop=params.stop,
+ logprobs=False,
+ seed=params.seed,
+ stream=True,
+ # This field is currently unsupported by the groq API
+ # stream_options={"include_usage": True},
+ presence_penalty=self.presence_penalty,
+ )
+ assert isinstance(api_result, AsyncStream)
+ logger.info("API response status code: {}", api_result.response.status_code)
+
+ yield LanguageModelStreamStartEvent()
+
+ usage = None
+ finish_reason: str | None = None
+ async for chunk in api_result:
+ if chunk.choices:
+ assert len(chunk.choices) == 1, "Currently only count=1 supported for streaming API."
+ data = only(chunk.choices)
+ delta = data.delta.content
+ if delta is not None:
+ yield LanguageModelStreamDeltaEvent(delta=delta)
+ if data.finish_reason:
+ finish_reason = str(data.finish_reason)
+
+ stop_reason = _GROQ_STOP_REASON_TO_STOP_REASON[str(finish_reason)]
+ # Note, Open API treats end turn and stop sequence the same TODO: check if groq is the same
+ # Here we assume it is stop sequence if user has specified a stop sequence
+ if params.stop is not None and stop_reason == ResponseStopReason.END_TURN:
+ yield LanguageModelStreamDeltaEvent(delta=params.stop)
+
+ if usage is not None:
+ completion_tokens = usage.completion_tokens
+ prompt_tokens = usage.prompt_tokens
+ dollars_used = self.calculate_cost(prompt_tokens, completion_tokens)
+ else:
+ completion_tokens = -1
+ prompt_tokens = -1
+ dollars_used = -1
+ logger.trace("dollars used: {}", dollars_used)
+
+ yield LanguageModelStreamEndEvent(
+ usage=LanguageModelResponseUsage(
+ prompt_tokens_used=prompt_tokens,
+ completion_tokens_used=completion_tokens,
+ dollars_used=dollars_used,
+ ),
+ stop_reason=stop_reason,
+ )
diff --git a/vet/imbue_core/agents/llm_apis/language_model_api.py b/vet/imbue_core/agents/llm_apis/language_model_api.py
@@ -0,0 +1,547 @@
+import abc
+import asyncio
+import contextvars
+import hashlib
+import inspect
+import os
+import random
+from pathlib import Path
+from types import FrameType
+from typing import AsyncGenerator
+from typing import Awaitable
+from typing import Callable
+from typing import TypeVar
+from typing import final
+from uuid import UUID
+from uuid import uuid4
+
+import anyio
+from loguru import logger
+
+from vet.imbue_core.agents.llm_apis.constants import approximate_token_count
+from vet.imbue_core.agents.llm_apis.data_types import CachedCostedLanguageModelResponse
+from vet.imbue_core.agents.llm_apis.data_types import CachedCostedModelResponse
+from vet.imbue_core.agents.llm_apis.data_types import CachingInfo
+from vet.imbue_core.agents.llm_apis.data_types import CostedLanguageModelResponse
+from vet.imbue_core.agents.llm_apis.data_types import CountTokensResponse
+from vet.imbue_core.agents.llm_apis.data_types import InputsT
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelCompleteInputs
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelGenerationParams
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelResponse
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelStreamInputs
+from vet.imbue_core.agents.llm_apis.data_types import ModelResponseT
+from vet.imbue_core.agents.llm_apis.errors import LanguageModelRetryLimitError
+from vet.imbue_core.agents.llm_apis.errors import PromptTooLongError
+from vet.imbue_core.agents.llm_apis.errors import TransientLanguageModelError
+from vet.imbue_core.agents.llm_apis.errors import UnsetCachePathError
+from vet.imbue_core.agents.llm_apis.models import ModelInfo
+from vet.imbue_core.agents.llm_apis.stream import LanguageModelStreamCallback
+from vet.imbue_core.agents.llm_apis.stream import LanguageModelStreamEvent
+from vet.imbue_core.agents.llm_apis.stream import PromptDebuggingCallback
+from vet.imbue_core.agents.llm_apis.stream import SettleSpendCallback
+from vet.imbue_core.agents.llm_apis.stream import StreamedLanguageModelResponse
+from vet.imbue_core.agents.llm_apis.stream import UpdateCacheCallback
+from vet.imbue_core.agents.llm_apis.stream import get_cached_response_stream
+from vet.imbue_core.agents.primitives.resource_limits import PaymentAuthorization
+from vet.imbue_core.agents.primitives.resource_limits import get_global_resource_limits
+from vet.imbue_core.async_utils import sync
+from vet.imbue_core.caching import AsyncCache
+from vet.imbue_core.cattrs_serialization import serialize_to_json
+from vet.imbue_core.pydantic_serialization import MutableModel
+
+# Context variable to disable caching.
+IS_LLM_CACHING_DISABLED_GLOBALLY = contextvars.ContextVar("is_llm_caching_disabled_globally", default=False)
+# Context variable for injecting a default seed which will become part of the LLM cache key.
+LLM_GLOBAL_DEFAULT_SEED = contextvars.ContextVar("llm_global_default_seed", default=0)
+
+# Maximum number of retries for network failures.
+MAX_RETRIES = 5
+
+
+EXCLUDED_CACHE_KEY_ARGS = ["self", "is_caching_enabled", "call_id"]
+
+
+def _create_base_cache_key_from_frame(frame: FrameType) -> str:
+ """Create a cache key from the args of a function by passing its frame."""
+ args, _, _, values = inspect.getargvalues(frame)
+ return "|".join(f"{arg}={values[arg]}" for arg in args if arg not in EXCLUDED_CACHE_KEY_ARGS)
+
+
+CostedResponseT = TypeVar("CostedResponseT", bound=CostedLanguageModelResponse | CountTokensResponse)
+FinalResponseT = TypeVar(
+ "FinalResponseT",
+ bound=CostedLanguageModelResponse | StreamedLanguageModelResponse | CountTokensResponse,
+)
+
+
+class LanguageModelAPI(abc.ABC, MutableModel):
+ model_name: str
+ cache_path: Path | None
+ is_caching_inputs: bool = False
+ is_running_offline: bool = False
+ is_conversational: bool = False
+ is_using_logprobs: bool = False
+
+ # retry/timeout values
+ retry_sleep_time: float = 2.0
+ retry_backoff_factor: float = 3.0
+ retry_jitter_factor: float = 0.5
+
+ # TODO: Consider storing the model_config here as well.
+
+ @property
+ @abc.abstractmethod
+ def model_info(self) -> ModelInfo: ...
+
+ def get_response_cache(self) -> AsyncCache[CachedCostedLanguageModelResponse]:
+ if self.cache_path is None:
+ raise UnsetCachePathError()
+ return AsyncCache(self.cache_path, CachedCostedLanguageModelResponse)
+
+ def _create_cache_key(self, base_key: str) -> str:
+ object_cache_attributes = self.model_dump(
+ exclude={
+ "cache_path",
+ "count_tokens_cache_path",
+ "base_url",
+ "api_key_env",
+ "context_window",
+ "max_output_tokens",
+ }
+ )
+ # have to reset the offline key to the same value so that that doesnt invalidate the cache
+ object_cache_attributes["is_running_offline"] = True
+ object_cache_attributes["__name__"] = self.__class__.__name__
+ base_key_md5 = hashlib.md5(base_key.encode()).hexdigest()
+ object_cache_attributes["__request_key_md5__"] = base_key_md5
+ return serialize_to_json(object_cache_attributes)
+
+ async def check_cache_core(
+ self,
+ cache_getter: Callable[[], AsyncCache[CachedCostedModelResponse[InputsT, ModelResponseT]]],
+ cache_key: str,
+ ) -> ModelResponseT | None:
+ async with cache_getter() as cache:
+ cached_result = await cache.get(cache_key)
+
+ if cached_result is not None:
+ if cached_result.error:
+ if cached_result.error.startswith(PromptTooLongError.__name__):
+ raise PromptTooLongError.from_string(cached_result.error)
+ raise Exception(f"Unknown cached result error type: {cached_result.error}")
+ assert cached_result.response is not None
+ return cached_result.response
+ return None
+
+ async def check_cache(self, cache_key: str) -> CostedLanguageModelResponse | None:
+ return await self.check_cache_core(self.get_response_cache, cache_key)
+
+ async def _get_auth(self, prompt: str, max_tokens: int | None) -> PaymentAuthorization | None:
+ global_resource_limits = get_global_resource_limits()
+ if global_resource_limits is not None:
+ prompt_tokens = self.count_tokens(prompt)
+ completion_tokens = max_tokens if max_tokens is not None else self.get_max_completion_size_in_tokens()
+ upper_bound_cost_estimate = self.estimate_cost(prompt_tokens, completion_tokens)
+ assert global_resource_limits is not None
+ auth: PaymentAuthorization = await global_resource_limits.authorize_spend(
+ upper_bound_cost_estimate,
+ debug_info={
+ "model_name": self.model_name,
+ "prompt_tokens": prompt_tokens,
+ "completion_tokens": completion_tokens,
+ },
+ )
+ return auth
+
+ if "PYTEST_CURRENT_TEST" not in os.environ:
+ logger.warning(
+ "You are trying to call a language model with no global resource limits set. That is a bad idea because the spend will not be restricted, and you may end up accidentally spending much more than you expected."
+ )
+ return None
+
+ async def _settle_spend(self, auth: PaymentAuthorization, dollars_used: float) -> None:
+ global_resource_limits = get_global_resource_limits()
+ assert global_resource_limits is not None
+ await global_resource_limits.settle_spend(auth, dollars_used)
+ return None
+
+ def assert_caching_enabled_if_offline(self, is_caching_enabled: bool) -> None:
+ if self.is_running_offline:
+ assert is_caching_enabled, "Caching must be enabled when running offline"
+
+ def assert_not_offline_if_cache_miss(self, prompt: str) -> None:
+ max_n_chars = 50
+ prompt_stub = prompt[:max_n_chars] + ("..." if len(prompt) > max_n_chars else "")
+ assert (
+ not self.is_running_offline
+ ), f"Running offline but did not have a cached response for this query! Prompt: {prompt_stub}"
+
+ async def complete(
+ self,
+ prompt: str,
+ params: LanguageModelGenerationParams,
+ is_caching_enabled: bool = True,
+ ) -> tuple[LanguageModelResponse, ...]:
+ call_id = uuid4()
+ logger.trace(
+ "[{call_id}] Calling complete with params: {params} and is_caching_enabled={is_caching_enabled} and {prompt}",
+ call_id=call_id,
+ params=params,
+ is_caching_enabled=is_caching_enabled,
+ prompt=prompt[:40],
+ )
+ if _complete_concurrency_hook_fn is not None:
+ await _complete_concurrency_hook_fn(self)
+
+ is_caching_enabled_with_override = is_caching_enabled and not IS_LLM_CACHING_DISABLED_GLOBALLY.get()
+ if params.seed is None:
+ params = params.evolve(params.ref().seed, LLM_GLOBAL_DEFAULT_SEED.get())
+
+ return await self._complete(
+ prompt,
+ params,
+ is_caching_enabled=is_caching_enabled_with_override,
+ call_id=call_id,
+ )
+
+ complete_sync = sync(complete)
+
+ async def complete_with_usage(
+ self,
+ prompt: str,
+ params: LanguageModelGenerationParams,
+ is_caching_enabled: bool = True,
+ ) -> CostedLanguageModelResponse:
+ call_id = uuid4()
+ if _complete_concurrency_hook_fn is not None:
+ await _complete_concurrency_hook_fn(self)
+
+ is_caching_enabled_with_override = is_caching_enabled and not IS_LLM_CACHING_DISABLED_GLOBALLY.get()
+ if params.seed is None:
+ params = params.evolve(params.ref().seed, LLM_GLOBAL_DEFAULT_SEED.get())
+
+ return await self._complete_with_usage(
+ prompt,
+ params,
+ is_caching_enabled=is_caching_enabled_with_override,
+ call_id=call_id,
+ )
+
+ complete_with_usage_sync = sync(complete_with_usage)
+
+ async def _complete_with_usage(
+ self,
+ prompt: str,
+ params: LanguageModelGenerationParams,
+ is_caching_enabled: bool,
+ call_id: UUID,
+ ) -> CostedLanguageModelResponse:
+ self._warn_if_no_stop_condition_and_not_conversational(params)
+ self.assert_caching_enabled_if_offline(is_caching_enabled)
+
+ frame: FrameType | None = None
+ if is_caching_enabled:
+ frame = inspect.currentframe()
+
+ costed_response_to_output: Callable[[CostedLanguageModelResponse], CostedLanguageModelResponse] = lambda cr: cr
+
+ cache_key: str | None = None
+ if is_caching_enabled:
+ cache_key, cached_response = await self._get_from_cache(frame, costed_response_to_output)
+
+ if cached_response is not None:
+ return cached_response
+
+ self.assert_not_offline_if_cache_miss(prompt)
+
+ auth = await self._get_auth(prompt, params.max_tokens)
+
+ sleep_time = self.retry_sleep_time
+ last_error_msg: str | None = None
+ for network_failure_count in range(MAX_RETRIES):
+ try:
+ api_inputs = LanguageModelCompleteInputs(
+ prompt=prompt,
+ params=params,
+ network_failure_count=network_failure_count,
+ )
+ response = await self._call_api_one_arg(api_inputs)
+ if is_caching_enabled:
+ assert cache_key is not None
+ result = CachedCostedLanguageModelResponse(
+ response=response,
+ inputs=api_inputs if self.is_caching_inputs else None,
+ )
+ async with self.get_response_cache() as cache:
+ await cache.set(cache_key, result)
+
+ if auth is not None:
+ await self._settle_spend(auth, response.usage.dollars_used)
+
+ return response
+
+ except PromptTooLongError as e:
+ logger.trace(
+ "[{call_id}] Prompt too long error in model {model_name}",
+ call_id=call_id,
+ model_name=self.model_name,
+ )
+ if is_caching_enabled:
+ assert cache_key is not None
+ async with self.get_response_cache() as cache:
+ await cache.set(
+ cache_key,
+ CachedCostedLanguageModelResponse(error=e.to_string()),
+ )
+ raise
+ except TransientLanguageModelError as e:
+ last_error_msg = str(e)
+ if network_failure_count < MAX_RETRIES - 1:
+ if self.retry_jitter_factor > 0:
+ max_jitter = sleep_time * self.retry_jitter_factor
+ sleep_time += random.uniform(-max_jitter / 2, max_jitter / 2)
+ logger.debug(
+ f"Transient language model error ({str(e)}) in model {self.model_name}, retrying with sleep time {sleep_time} seconds..."
+ )
+ await asyncio.sleep(sleep_time)
+ sleep_time *= self.retry_backoff_factor
+ raise LanguageModelRetryLimitError(last_error_msg or "Unknown error (this should not happen)")
+
+ async def _complete(
+ self,
+ prompt: str,
+ params: LanguageModelGenerationParams,
+ is_caching_enabled: bool,
+ call_id: UUID,
+ ) -> tuple[LanguageModelResponse, ...]:
+ # Delegate to _complete_with_usage and extract just the responses
+ # May have more than count responses cached, so just return first count responses
+ costed_response = await self._complete_with_usage(prompt, params, is_caching_enabled, call_id)
+ return costed_response.responses[: params.count]
+
+ @final
+ async def _call_api_one_arg(self, api_inputs: LanguageModelCompleteInputs) -> CostedLanguageModelResponse:
+ """Delegates to the abstract method _call_api, which must be implemented by subclasses."""
+ return await self._call_api(
+ prompt=api_inputs.prompt,
+ params=api_inputs.params,
+ network_failure_count=api_inputs.network_failure_count,
+ )
+
+ @abc.abstractmethod
+ async def _call_api(
+ self,
+ prompt: str,
+ params: LanguageModelGenerationParams,
+ # this is used to track how many times we've retried due to network failures, since we want the return type to contain that information
+ network_failure_count: int = 0,
+ ) -> CostedLanguageModelResponse:
+ """If defined, the stop sequence should be part of the sequence (if it was actually generated)"""
+
+ async def stream(
+ self,
+ prompt: str,
+ is_caching_enabled: bool = True,
+ params: LanguageModelGenerationParams = LanguageModelGenerationParams(),
+ ) -> StreamedLanguageModelResponse:
+ if params.seed is None:
+ params = params.evolve(params.ref().seed, LLM_GLOBAL_DEFAULT_SEED.get())
+
+ is_caching_enabled_with_override = is_caching_enabled and not IS_LLM_CACHING_DISABLED_GLOBALLY.get()
+
+ return await self._stream(
+ prompt=prompt,
+ is_caching_enabled=is_caching_enabled_with_override,
+ params=params,
+ )
+
+ async def _stream(
+ self,
+ prompt: str,
+ is_caching_enabled: bool,
+ params: LanguageModelGenerationParams,
+ ) -> StreamedLanguageModelResponse:
+ assert params.count == 1, "Stream API currently only supports count=1 due to limitations of some APIs."
+
+ self._warn_if_no_stop_condition_and_not_conversational(params)
+ self.assert_caching_enabled_if_offline(is_caching_enabled)
+
+ frame: FrameType | None = None
+ if is_caching_enabled:
+ frame = inspect.currentframe()
+
+ # Note it's technically possible multiple responses cached for given prompt (e.g. from call to complete())
+ # for now we just return first one
+ costed_response_to_output = lambda cr: StreamedLanguageModelResponse(
+ get_cached_response_stream(cr),
+ network_failure_count=0,
+ completion_callbacks=(),
+ )
+ cache_key: str | None = None
+ if is_caching_enabled:
+ cache_key, cached_response = await self._get_from_cache(frame, costed_response_to_output)
+
+ if cached_response is not None:
+ return cached_response
+
+ self.assert_not_offline_if_cache_miss(prompt)
+
+ auth = await self._get_auth(prompt, params.max_tokens)
+
+ sleep_time = self.retry_sleep_time
+ last_error_msg: str | None = None
+ for network_failure_count in range(MAX_RETRIES):
+ # Loop until success or an exception is raised
+ try:
+ api_inputs = LanguageModelStreamInputs(prompt=prompt, params=params)
+ api_stream = await self._get_api_stream_one_arg(api_inputs)
+ callbacks: list[LanguageModelStreamCallback] = []
+ if is_caching_enabled:
+ assert cache_key is not None
+ cache = self.get_response_cache()
+ callbacks.append(
+ UpdateCacheCallback(
+ key=cache_key,
+ cache=cache,
+ api_inputs=api_inputs if self.is_caching_inputs else None,
+ )
+ )
+ llm_debug_output_folder = os.getenv("LLM_DEBUG_PATH", None)
+ if llm_debug_output_folder is not None:
+ output_path = anyio.Path(llm_debug_output_folder) / f"{uuid4()}.txt"
+ # write out the prompt (helps with debugging so we can see when things blow up)
+ await output_path.write_text(prompt)
+ # overwrite the file with the prompt and completion when done
+ callbacks.append(PromptDebuggingCallback(prompt=prompt, output_path=output_path))
+
+ if auth is not None:
+ callbacks.append(SettleSpendCallback(auth=auth))
+
+ return StreamedLanguageModelResponse(
+ api_stream,
+ network_failure_count=network_failure_count,
+ completion_callbacks=callbacks,
+ )
+
+ except PromptTooLongError as e:
+ if is_caching_enabled:
+ assert cache_key is not None
+ async with self.get_response_cache() as cache:
+ await cache.set(
+ cache_key,
+ CachedCostedLanguageModelResponse(error=e.to_string()),
+ )
+ raise
+ except TransientLanguageModelError as e:
+ last_error_msg = str(e)
+ if network_failure_count < MAX_RETRIES - 1:
+ if self.retry_jitter_factor > 0:
+ sleep_time += random.uniform(0, sleep_time * self.retry_jitter_factor)
+ logger.debug(
+ f"Transient language model error ({str(e)}) in model {self.model_name}, retrying with sleep time {sleep_time} seconds..."
+ )
+ await asyncio.sleep(sleep_time)
+ sleep_time *= self.retry_backoff_factor
+ raise LanguageModelRetryLimitError(last_error_msg or "Unknown error (this should not happen)")
+
+ @final
+ async def _get_api_stream_one_arg(
+ self, api_inputs: LanguageModelStreamInputs
+ ) -> AsyncGenerator[LanguageModelStreamEvent, None]:
+ """Delegates to the abstract method _get_api_stream, which must be implemented by subclasses."""
+ return self._get_api_stream(prompt=api_inputs.prompt, params=api_inputs.params)
+
+ @abc.abstractmethod
+ def _get_api_stream(
+ self,
+ prompt: str,
+ params: LanguageModelGenerationParams,
+ ) -> AsyncGenerator[LanguageModelStreamEvent, None]:
+ """If defined, the stop sequence should be part of the sequence (if it was actually generated)"""
+
+ def _warn_if_no_stop_condition_and_not_conversational(self, params: LanguageModelGenerationParams) -> None:
+ if (params.stop is None and params.max_tokens is None) and not self.is_conversational:
+ logger.debug(
+ "Did not specify either `max_tokens` or `stop`, and this is not a conversational model. The completion will go until the entire context window is filled. Preferably you don't do this, because it is fairly inefficient."
+ )
+
+ async def _get_from_cache_core(
+ self,
+ frame: FrameType | None,
+ costed_response_to_output: Callable[[CostedResponseT], FinalResponseT],
+ cache_checker: Callable[[str], Awaitable[CostedResponseT | None]],
+ ) -> tuple[str | None, FinalResponseT | None]:
+ cache_key: str | None
+
+ cache_key, costed_response = await self._get_costed_response_from_frame_core(cache_checker, frame)
+
+ if costed_response is not None:
+ return cache_key, costed_response_to_output(costed_response)
+ return cache_key, None
+
+ async def _get_from_cache(
+ self,
+ frame: FrameType | None,
+ costed_response_to_output: Callable[[CostedLanguageModelResponse], FinalResponseT],
+ ) -> tuple[str | None, FinalResponseT | None]:
+ return await self._get_from_cache_core(frame, costed_response_to_output, self.check_cache)
+
+ async def _get_costed_response_from_frame_core(
+ self,
+ cache_checker: Callable[[str], Awaitable[CostedResponseT | None]],
+ frame: FrameType | None,
+ ) -> tuple[str, CostedResponseT | None]:
+ assert frame is not None
+ cache_key = self._create_cache_key(_create_base_cache_key_from_frame(frame))
+ costed_response = await cache_checker(cache_key)
+ return cache_key, costed_response
+
+ def count_tokens(self, text: str) -> int:
+ # this is VERY approximate, but many of the child models have nothing, so...
+ return approximate_token_count(text)
+
+ def basic_calculate_cost(self, prompt_tokens: int, completion_tokens: int) -> float:
+ return (
+ prompt_tokens * self.model_info.cost_per_input_token
+ + completion_tokens * self.model_info.cost_per_output_token
+ )
+
+ def estimate_cost(self, prompt_tokens: int, completion_tokens: int) -> float:
+ """Estimate the cost of a request before it has been made. Doesn't use any caching info."""
+ return self.basic_calculate_cost(prompt_tokens, completion_tokens)
+
+ def calculate_cost(
+ self,
+ prompt_tokens: int,
+ completion_tokens: int,
+ caching_info: CachingInfo | None = None,
+ ) -> float:
+ """Overridden by subclasses which have more complex cost calculations, such as if caching is used."""
+ logger.info(
+ f"no calculate_cost implemented for {self.model_name}; using basic_calculate_cost",
+ model_name=self.model_name,
+ )
+ return self.basic_calculate_cost(prompt_tokens, completion_tokens)
+
+ def get_max_completion_size_in_tokens(self) -> int:
+ if self.model_info.max_output_tokens is not None:
+ return self.model_info.max_output_tokens
+ # assume max output is just the context window size
+ return self.model_info.max_input_tokens
+
+ def get_max_prompt_size_in_tokens(self) -> int:
+ return self.model_info.max_input_tokens
+
+ def get_context_window_size_in_tokens(self) -> int:
+ return self.get_max_completion_size_in_tokens() + self.get_max_prompt_size_in_tokens()
+
+
+COMPLETE_CONCURRENCY_HOOK_FN = Callable[[LanguageModelAPI], Awaitable[None]] | None
+_complete_concurrency_hook_fn: COMPLETE_CONCURRENCY_HOOK_FN = None
+
+
+def set_language_model_api_complete_concurrency_hook(
+ hook_fn: COMPLETE_CONCURRENCY_HOOK_FN,
+) -> None:
+ global _complete_concurrency_hook_fn
+ _complete_concurrency_hook_fn = hook_fn
diff --git a/vet/imbue_core/agents/llm_apis/mock_api.py b/vet/imbue_core/agents/llm_apis/mock_api.py
@@ -0,0 +1,193 @@
+import asyncio
+import enum
+from pathlib import Path
+from typing import AsyncGenerator
+
+import toml
+from loguru import logger
+from pydantic.fields import Field
+from pydantic.functional_validators import field_validator
+
+from vet.imbue_core.agents.llm_apis.data_types import CostedLanguageModelResponse
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelGenerationParams
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelResponse
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelResponseUsage
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelResponseWithLogits
+from vet.imbue_core.agents.llm_apis.data_types import ResponseStopReason
+from vet.imbue_core.agents.llm_apis.data_types import TokenProbability
+from vet.imbue_core.agents.llm_apis.language_model_api import LanguageModelAPI
+from vet.imbue_core.agents.llm_apis.models import ModelInfo
+from vet.imbue_core.agents.llm_apis.stream import LanguageModelStreamDeltaEvent
+from vet.imbue_core.agents.llm_apis.stream import LanguageModelStreamEndEvent
+from vet.imbue_core.agents.llm_apis.stream import LanguageModelStreamEvent
+from vet.imbue_core.itertools import only
+from vet.imbue_core.pydantic_serialization import MutableModel
+
+
+class MockModelName(enum.StrEnum):
+ MOCK_MODEL = "my-mock-model"
+
+
+MY_MOCK_MODEL_INFO = ModelInfo(
+ model_name=MockModelName.MOCK_MODEL,
+ cost_per_input_token=0.0 / 1_000_000,
+ cost_per_output_token=0.0 / 1_000_000,
+ max_input_tokens=32_768,
+ max_output_tokens=None,
+)
+
+
+class Stats(MutableModel):
+ complete_calls: int = 0
+
+
+class LanguageModelMock(LanguageModelAPI):
+ model_name: str = MY_MOCK_MODEL_INFO.model_name
+ cache_path: Path | None = None
+ # FIXME: can't have a mutable class inside a frozen pydantic model
+ stats: Stats = Field(default_factory=Stats)
+
+ @property
+ def model_info(self) -> ModelInfo:
+ return MY_MOCK_MODEL_INFO
+
+ async def complete(
+ self,
+ prompt: str,
+ params: LanguageModelGenerationParams,
+ is_caching_enabled: bool = True,
+ ) -> tuple[LanguageModelResponse, ...]:
+ raise NotImplementedError()
+
+ def _get_token_probabilities(self, response_text: str) -> tuple[tuple[TokenProbability, ...], ...]:
+ return tuple(
+ (TokenProbability(token=pseudo_token, log_probability=0.0, is_stop=False),)
+ for pseudo_token in response_text.split()
+ )
+
+ async def _call_api(
+ self,
+ prompt: str,
+ params: LanguageModelGenerationParams,
+ network_failure_count: int = 0,
+ ) -> CostedLanguageModelResponse:
+ raise NotImplementedError()
+
+ def _get_api_stream(
+ self,
+ prompt: str,
+ params: LanguageModelGenerationParams,
+ ) -> AsyncGenerator[LanguageModelStreamEvent, None]:
+ # TODO Implement streaming support (?)
+ raise NotImplementedError()
+
+
+MOCK_STREAM_SLEEP_TIME = 5.0
+
+
+class FileBasedLanguageModelMock(LanguageModelMock):
+ """
+ A mock LLM API that reads responses from a toml file.
+ The response can either be identified using the toml key or a prompt which is part of the toml dictionary.
+ """
+
+ calls: int = 0
+
+ @field_validator("cache_path") # pyre-ignore[56]: pyre doesn't understand pydantic
+ @classmethod
+ def validate_cache_path(cls, v: Path | None) -> Path | None:
+ if v is None:
+ raise ValueError("Mock responses file path is not set.")
+ if not v.exists():
+ raise ValueError(f"Mock responses file {v} does not exist.")
+ if not v.suffix == ".toml":
+ raise ValueError(f"Mock responses file {v} is not a toml file.")
+ return v
+
+ def _get_user_message_from_prompt(self, prompt: str) -> str:
+ user_prompt = prompt.rsplit("[ROLE=USER]", 1)[-1].strip()
+ return user_prompt
+
+ def get_single_response(self, prompt: str) -> str:
+ return only(self.get_parts_of_response(prompt))
+
+ def get_parts_of_response(self, prompt: str) -> tuple[str, ...]:
+ """
+ Support both of the following possible identifiers:
+ [identifier]
+ prompt = "user message here"
+ [[identifier.responses]]
+ text = "response"
+ [[identifier.responses]]
+ text = "response2"
+
+ [identifier]
+ prompt = "user message here"
+ response = "response"
+ """
+ # this is checked during validation but i guess the type checker doesn't see it
+ assert self.cache_path is not None
+ # TODO: should we try something that is not toml? toml formatting is a little annoying
+ toml_dict = toml.load(self.cache_path)
+ # TODO: currently the identifier is the last user message, because the entire prompt is really long
+ # if we need to support the same user message with different responses, expand this, maybe chat history?
+ identifier = self._get_user_message_from_prompt(prompt)
+ logger.info("Getting response for identifier: {} from {}", identifier, toml_dict)
+ toml_item = toml_dict.get(identifier, None)
+ if toml_item is None:
+ for toml_key, response in toml_dict.items():
+ if "prompt" in response:
+ if response["prompt"] == identifier:
+ toml_item = response
+ break
+ if toml_item is None:
+ raise KeyError(f"No response found for the given identifier {identifier}")
+
+ if "responses" in toml_item:
+ responses = toml_item["responses"]
+ if isinstance(responses, list):
+ return tuple(r["text"] for r in responses if isinstance(r, dict) and "text" in r)
+ raise ValueError(f"Expected 'responses' to be a list of tables in section '{identifier}'")
+
+ if "response" in toml_item:
+ return (str(toml_item["response"]),)
+
+ raise ValueError(f"No valid response or responses found for identifier '{identifier}'")
+
+ async def complete(
+ self,
+ prompt: str,
+ params: LanguageModelGenerationParams,
+ is_caching_enabled: bool = True,
+ ) -> tuple[LanguageModelResponse, ...]:
+ response = self.get_single_response(prompt)
+ self.stats.complete_calls += 1
+ token_probabilities = self._get_token_probabilities(response)
+ return (
+ LanguageModelResponseWithLogits(
+ text=response,
+ token_count=len(token_probabilities),
+ stop_reason=ResponseStopReason.NONE,
+ network_failure_count=0,
+ token_probabilities=token_probabilities,
+ ),
+ )
+
+ async def _get_api_stream(
+ self,
+ prompt: str,
+ params: LanguageModelGenerationParams,
+ ) -> AsyncGenerator[LanguageModelStreamEvent, None]:
+ responses = self.get_parts_of_response(prompt)
+ self.stats.complete_calls += 1
+ if len(responses) == 1:
+ response = responses[0]
+ yield LanguageModelStreamDeltaEvent(delta=response)
+ else:
+ for response in responses:
+ yield LanguageModelStreamDeltaEvent(delta=response)
+ await asyncio.sleep(MOCK_STREAM_SLEEP_TIME)
+ yield LanguageModelStreamEndEvent(
+ usage=LanguageModelResponseUsage(prompt_tokens_used=0, completion_tokens_used=0, dollars_used=0),
+ stop_reason=ResponseStopReason.NONE,
+ )
diff --git a/vet/imbue_core/agents/llm_apis/models.py b/vet/imbue_core/agents/llm_apis/models.py
@@ -0,0 +1,17 @@
+from vet.imbue_core.agents.llm_apis.union_data_types import ProviderSpecificModelInfoUnion
+from vet.imbue_core.pydantic_serialization import SerializableModel
+
+
+class ModelInfo(SerializableModel):
+ model_name: str
+ cost_per_input_token: float
+ cost_per_output_token: float
+ max_input_tokens: int
+ max_output_tokens: int | None
+ # requests per second
+ rate_limit_req: float | None = None
+ # tokens per second
+ rate_limit_tok: float | None = None
+ rate_limit_output_tok: float | None = None
+ max_thinking_budget: int | None = None
+ provider_specific_info: ProviderSpecificModelInfoUnion | None = None
diff --git a/vet/imbue_core/agents/llm_apis/openai_api.py b/vet/imbue_core/agents/llm_apis/openai_api.py
@@ -0,0 +1,654 @@
+import asyncio
+import enum
+import re
+from collections import defaultdict
+from contextlib import contextmanager
+from functools import lru_cache
+from typing import AsyncGenerator
+from typing import Iterator
+from typing import Mapping
+
+import httpx
+import tiktoken
+from loguru import logger
+from openai import AsyncStream
+from openai import InternalServerError
+from openai import NOT_GIVEN
+from openai import NotGiven
+from openai._client import AsyncOpenAI
+from openai._exceptions import APIConnectionError
+from openai._exceptions import BadRequestError
+from openai._exceptions import RateLimitError
+from openai.types.chat import ChatCompletion
+from pydantic.functional_validators import field_validator
+
+from vet.imbue_core.agents.llm_apis.api_utils import convert_prompt_to_openai_messages
+from vet.imbue_core.agents.llm_apis.data_types import CachingInfo
+from vet.imbue_core.agents.llm_apis.data_types import CostedLanguageModelResponse
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelGenerationParams
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelResponse
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelResponseUsage
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelResponseWithLogits
+from vet.imbue_core.agents.llm_apis.data_types import ResponseStopReason
+from vet.imbue_core.agents.llm_apis.data_types import TokenProbability
+from vet.imbue_core.agents.llm_apis.errors import BadAPIRequestError
+from vet.imbue_core.agents.llm_apis.errors import LanguageModelInvalidModelNameError
+from vet.imbue_core.agents.llm_apis.errors import MissingAPIKeyError
+from vet.imbue_core.agents.llm_apis.errors import PromptTooLongError
+from vet.imbue_core.agents.llm_apis.errors import TransientLanguageModelError
+from vet.imbue_core.agents.llm_apis.models import ModelInfo
+from vet.imbue_core.agents.llm_apis.openai_compatible_api import OpenAICompatibleAPI
+from vet.imbue_core.agents.llm_apis.openai_compatible_api import (
+ _OPENAI_COMPATIBLE_STOP_REASON_TO_STOP_REASON,
+)
+from vet.imbue_core.agents.llm_apis.openai_data_types import OpenAICachingInfo
+from vet.imbue_core.agents.llm_apis.stream import LanguageModelStreamDeltaEvent
+from vet.imbue_core.agents.llm_apis.stream import LanguageModelStreamEndEvent
+from vet.imbue_core.agents.llm_apis.stream import LanguageModelStreamEvent
+from vet.imbue_core.agents.llm_apis.stream import LanguageModelStreamStartEvent
+from vet.imbue_core.frozen_utils import FrozenDict
+from vet.imbue_core.frozen_utils import FrozenMapping
+from vet.imbue_core.itertools import only
+from vet.imbue_core.secrets_utils import get_secret
+
+# note: we require that these model versions are explicit, just like the rest of our dependencies
+# the reason is that these models are actually now mostly deterministic, and it is much easier to debug if we know what model was used
+# also, there's no need to troll yourself by wondering why results have improved (or gotten worse) when you dont realized that the version has shifted under you
+# if you want to use an upgraded model, just upgrade the model to the key displayed here: https://platform.openai.com/docs/models/overview
+# please do NOT set these back to the generic model names!
+
+FINE_TUNED_GPT4O_MINI_2024_07_18_PREFIX = "ft:gpt-4o-mini-2024-07-18"
+FINE_TUNED_GPT4O_2024_08_06_PREFIX = "ft:gpt-4o-2024-08-06"
+
+
+class OpenAIModelName(enum.StrEnum):
+ GPT_3_5_TURBO = "gpt-3.5-turbo-0125"
+ GPT_4_0613 = "gpt-4-0613"
+ GPT_4_1106_PREVIEW = "gpt-4-1106-preview"
+ GPT_4_0125_PREVIEW = "gpt-4-0125-preview"
+ GPT_4_TURBO_2024_04_09 = "gpt-4-turbo-2024-04-09"
+ GPT_4O_2024_05_13 = "gpt-4o-2024-05-13"
+ GPT_4O_2024_08_06 = "gpt-4o-2024-08-06"
+ GPT_4O_MINI_2024_07_18 = "gpt-4o-mini-2024-07-18"
+ O1_2024_12_17 = "o1-2024-12-17"
+ GPT_4_1_2025_04_14 = "gpt-4.1-2025-04-14"
+ GPT_4_1_MINI_2025_04_14 = "gpt-4.1-mini-2025-04-14"
+ GPT_4_1_NANO_2025_04_14 = "gpt-4.1-nano-2025-04-14"
+ O3_2025_04_16 = "o3-2025-04-16"
+ O3_MINI_2025_01_31 = "o3-mini-2025-01-31"
+ O4_MINI_2025_04_16 = "o4-mini-2025-04-16"
+ GPT_5_2025_08_07 = "gpt-5-2025-08-07"
+ GPT_5_MINI_2025_08_07 = "gpt-5-mini-2025-08-07"
+ GPT_5_NANO_2025_08_07 = "gpt-5-nano-2025-08-07"
+ GPT_5_1_2025_11_13 = "gpt-5.1-2025-11-13"
+
+
+# Using Tier 5 rate limits
+# https://platform.openai.com/settings/organization/limits
+
+OPENAI_MODEL_INFO_BY_NAME: FrozenMapping[OpenAIModelName, ModelInfo] = FrozenDict(
+ {
+ OpenAIModelName.GPT_3_5_TURBO: ModelInfo(
+ model_name=str(OpenAIModelName.GPT_3_5_TURBO),
+ cost_per_input_token=0.5 / 1_000_000,
+ cost_per_output_token=1.5 / 1_000_000,
+ max_input_tokens=16_385,
+ max_output_tokens=4096,
+ rate_limit_req=10000 / 60, # 10000 RPM = 166.67 RPS
+ ),
+ OpenAIModelName.GPT_4_0613: ModelInfo(
+ model_name=str(OpenAIModelName.GPT_4_0613),
+ cost_per_input_token=30.0 / 1_000_000,
+ cost_per_output_token=60.0 / 1_000_000,
+ max_input_tokens=8192,
+ max_output_tokens=8192,
+ rate_limit_req=10000 / 60, # 10000 RPM = 166.67 RPS
+ ),
+ OpenAIModelName.GPT_4_1106_PREVIEW: ModelInfo( # Cannot find this model
+ model_name=str(OpenAIModelName.GPT_4_1106_PREVIEW),
+ cost_per_input_token=10.0 / 1_000_000,
+ cost_per_output_token=30.0 / 1_000_000,
+ max_input_tokens=128_000,
+ max_output_tokens=4096,
+ rate_limit_req=10000 / 60, # 10000 RPM = 166.67 RPS
+ ),
+ OpenAIModelName.GPT_4_0125_PREVIEW: ModelInfo(
+ model_name=str(OpenAIModelName.GPT_4_0125_PREVIEW),
+ cost_per_input_token=10.0 / 1_000_000,
+ cost_per_output_token=30.0 / 1_000_000,
+ max_input_tokens=128_000,
+ max_output_tokens=4096,
+ rate_limit_req=10000 / 60, # 10000 RPM = 166.67 RPS
+ ),
+ OpenAIModelName.GPT_4_TURBO_2024_04_09: ModelInfo(
+ model_name=str(OpenAIModelName.GPT_4_TURBO_2024_04_09),
+ cost_per_input_token=10.0 / 1_000_000,
+ cost_per_output_token=30.0 / 1_000_000,
+ max_input_tokens=128_000,
+ max_output_tokens=4096,
+ rate_limit_req=10000 / 60, # 10000 RPM = 166.67 RPS
+ ),
+ OpenAIModelName.GPT_4O_2024_05_13: ModelInfo(
+ model_name=str(OpenAIModelName.GPT_4O_2024_05_13),
+ cost_per_input_token=5.0 / 1_000_000,
+ cost_per_output_token=15.0 / 1_000_000,
+ max_input_tokens=128_000,
+ max_output_tokens=4096,
+ rate_limit_req=10000 / 60, # 10000 RPM = 166.67 RPS
+ ),
+ OpenAIModelName.GPT_4O_2024_08_06: ModelInfo(
+ model_name=str(OpenAIModelName.GPT_4O_2024_08_06),
+ cost_per_input_token=2.5 / 1_000_000,
+ cost_per_output_token=10.0 / 1_000_000,
+ max_input_tokens=128_000,
+ max_output_tokens=16_384,
+ rate_limit_req=10000 / 60, # 10000 RPM = 166.67 RPS
+ ),
+ OpenAIModelName.GPT_4O_MINI_2024_07_18: ModelInfo(
+ model_name=str(OpenAIModelName.GPT_4O_MINI_2024_07_18),
+ cost_per_input_token=0.15 / 1_000_000,
+ cost_per_output_token=0.60 / 1_000_000,
+ max_input_tokens=128_000,
+ max_output_tokens=16_384,
+ rate_limit_req=30000 / 60, # 30000 RPM = 500 RPS
+ ),
+ OpenAIModelName.O1_2024_12_17: ModelInfo(
+ model_name=str(OpenAIModelName.O1_2024_12_17),
+ cost_per_input_token=15 / 1_000_000,
+ cost_per_output_token=60 / 1_000_000,
+ max_input_tokens=200_000,
+ max_output_tokens=100_000,
+ rate_limit_req=10000 / 60, # 10000 RPM = 166.67 RPS
+ ),
+ OpenAIModelName.GPT_4_1_2025_04_14: ModelInfo(
+ model_name=str(OpenAIModelName.GPT_4_1_2025_04_14),
+ cost_per_input_token=2 / 1_000_000,
+ cost_per_output_token=8 / 1_000_000,
+ max_input_tokens=1_047_576,
+ max_output_tokens=32_768,
+ rate_limit_req=10000 / 60, # 10000 RPM = 166.67 RPS
+ ),
+ OpenAIModelName.GPT_4_1_MINI_2025_04_14: ModelInfo(
+ model_name=str(OpenAIModelName.GPT_4_1_MINI_2025_04_14),
+ cost_per_input_token=0.4 / 1_000_000,
+ cost_per_output_token=1.6 / 1_000_000,
+ max_input_tokens=1_047_576,
+ max_output_tokens=32_768,
+ rate_limit_req=30000 / 60, # 30000 RPM = 500 RPS
+ ),
+ OpenAIModelName.GPT_4_1_NANO_2025_04_14: ModelInfo(
+ model_name=str(OpenAIModelName.GPT_4_1_NANO_2025_04_14),
+ cost_per_input_token=0.1 / 1_000_000,
+ cost_per_output_token=0.4 / 1_000_000,
+ max_input_tokens=1_047_576,
+ max_output_tokens=32_768,
+ rate_limit_req=30000 / 60, # 30000 RPM = 500 RPS
+ ),
+ OpenAIModelName.O4_MINI_2025_04_16: ModelInfo(
+ model_name=str(OpenAIModelName.O4_MINI_2025_04_16),
+ cost_per_input_token=1.1 / 1_000_000,
+ cost_per_output_token=4.4 / 1_000_000,
+ max_input_tokens=200_000,
+ max_output_tokens=100_000,
+ rate_limit_req=30000 / 60, # 30000 RPM = 500 RPS
+ ),
+ OpenAIModelName.O3_2025_04_16: ModelInfo(
+ model_name=str(OpenAIModelName.O3_2025_04_16),
+ cost_per_input_token=10 / 1_000_000,
+ cost_per_output_token=40 / 1_000_000,
+ max_input_tokens=200_000,
+ max_output_tokens=100_000,
+ rate_limit_req=10000 / 60, # 10000 RPM = 166.67 RPS
+ ),
+ OpenAIModelName.O3_MINI_2025_01_31: ModelInfo(
+ model_name=str(OpenAIModelName.O3_MINI_2025_01_31),
+ cost_per_input_token=1.1 / 1_000_000,
+ cost_per_output_token=4.4 / 1_000_000,
+ max_input_tokens=200_000,
+ max_output_tokens=100_000,
+ rate_limit_req=30000 / 60, # 30000 RPM = 500 RPS
+ ),
+ OpenAIModelName.GPT_5_2025_08_07: ModelInfo(
+ model_name=str(OpenAIModelName.GPT_5_2025_08_07),
+ cost_per_input_token=1.25 / 1_000_000,
+ cost_per_output_token=10 / 1_000_000,
+ max_input_tokens=400_000,
+ max_output_tokens=128_000,
+ rate_limit_req=15000 / 60, # 15000 RPM = 250 RPS
+ ),
+ OpenAIModelName.GPT_5_MINI_2025_08_07: ModelInfo(
+ model_name=str(OpenAIModelName.GPT_5_MINI_2025_08_07),
+ cost_per_input_token=0.25 / 1_000_000,
+ cost_per_output_token=2.00 / 1_000_000,
+ max_input_tokens=400_000,
+ max_output_tokens=128_000,
+ rate_limit_req=30000 / 60, # 30000 RPM = 500 RPS
+ ),
+ OpenAIModelName.GPT_5_NANO_2025_08_07: ModelInfo(
+ model_name=str(OpenAIModelName.GPT_5_NANO_2025_08_07),
+ cost_per_input_token=0.05 / 1_000_000,
+ cost_per_output_token=0.40 / 1_000_000,
+ max_input_tokens=400_000,
+ max_output_tokens=128_000,
+ rate_limit_req=30000 / 60, # 30000 RPM = 500 RPS
+ ),
+ OpenAIModelName.GPT_5_1_2025_11_13: ModelInfo(
+ model_name=str(OpenAIModelName.GPT_5_1_2025_11_13),
+ cost_per_input_token=1.25 / 1_000_000,
+ cost_per_output_token=10 / 1_000_000,
+ max_input_tokens=400_000,
+ max_output_tokens=128_000,
+ rate_limit_req=15000 / 60, # 15000 RPM = 250 RPS
+ ),
+ }
+)
+
+
+# Pricing for fine-tuned models taken from here: https://platform.openai.com/docs/pricing
+def get_model_info(model_name: OpenAIModelName) -> ModelInfo:
+ # Check for the family of fine-tuned models.
+ if model_name.startswith(FINE_TUNED_GPT4O_MINI_2024_07_18_PREFIX):
+ return ModelInfo(
+ model_name=str(model_name),
+ cost_per_input_token=0.3 / 1_000_000,
+ cost_per_output_token=1.2 / 1_000_000,
+ max_input_tokens=128_000,
+ max_output_tokens=16_384,
+ rate_limit_req=30000 / 60, # 30000 RPM = 500 RPS (same as base model)
+ )
+ if model_name.startswith(FINE_TUNED_GPT4O_2024_08_06_PREFIX):
+ return ModelInfo(
+ model_name=str(model_name),
+ cost_per_input_token=3.75 / 1_000_000,
+ cost_per_output_token=15.0 / 1_000_000,
+ max_input_tokens=128_000,
+ max_output_tokens=16_384,
+ rate_limit_req=10000 / 60, # 10000 RPM = 166.67 RPS (same as base model)
+ )
+ # Otherwise, return the model info for the base model.
+ return OPENAI_MODEL_INFO_BY_NAME[model_name]
+
+
+_CAPACITY_SEMAPHOR_BY_MODEL_NAME: Mapping[OpenAIModelName, asyncio.Semaphore] = defaultdict(
+ lambda: asyncio.Semaphore(20),
+ {
+ OpenAIModelName.GPT_3_5_TURBO: asyncio.Semaphore(100),
+ OpenAIModelName.GPT_4_0613: asyncio.Semaphore(60),
+ OpenAIModelName.GPT_4_1_NANO_2025_04_14: asyncio.Semaphore(80),
+ },
+)
+
+
+def _get_capacity_semaphor(model_name: OpenAIModelName) -> asyncio.Semaphore:
+ # Fine-tuned models share rate limits with the base model.
+ if model_name.startswith(FINE_TUNED_GPT4O_MINI_2024_07_18_PREFIX):
+ model_name = OpenAIModelName.GPT_4O_MINI_2024_07_18
+ elif model_name.startswith(FINE_TUNED_GPT4O_2024_08_06_PREFIX):
+ model_name = OpenAIModelName.GPT_4O_2024_08_06
+ return _CAPACITY_SEMAPHOR_BY_MODEL_NAME[model_name]
+
+
+def is_openai_reasoning_model(model_name: str) -> bool:
+ return model_name in (
+ OpenAIModelName.O1_2024_12_17,
+ OpenAIModelName.O4_MINI_2025_04_16,
+ OpenAIModelName.O3_2025_04_16,
+ OpenAIModelName.O3_MINI_2025_01_31,
+ OpenAIModelName.GPT_5_2025_08_07,
+ OpenAIModelName.GPT_5_MINI_2025_08_07,
+ OpenAIModelName.GPT_5_NANO_2025_08_07,
+ )
+
+
+def is_fine_tuned_openai_model(model_name: OpenAIModelName) -> bool:
+ return model_name.value.startswith(FINE_TUNED_GPT4O_MINI_2024_07_18_PREFIX) or model_name.value.startswith(
+ FINE_TUNED_GPT4O_2024_08_06_PREFIX
+ )
+
+
+_OPENAI_COMPLETION_ERROR_PATTERN = re.compile(
+ r".*This model's maximum context length is (\d+) tokens, however you requested (\d+) tokens \((\d+) in your prompt; (\d+) for the completion\). Please reduce your prompt; or completion length.*"
+)
+
+_OPENAI_STOP_REASON_TO_STOP_REASON = _OPENAI_COMPATIBLE_STOP_REASON_TO_STOP_REASON
+
+
+@lru_cache(maxsize=1)
+def get_openai_tokenizer(model_name: str) -> tiktoken.Encoding:
+ """Get the appropriate tiktoken tokenizer for an OpenAI model.
+
+ Args:
+ model_name: The OpenAI model name (e.g., "gpt-4o-2024-08-06").
+
+ Returns:
+ The tiktoken Encoding for the model.
+ """
+ if model_name.startswith("gpt-4"):
+ fixed_model_name = "gpt-4"
+ elif model_name.startswith("gpt-3.5"):
+ fixed_model_name = "gpt-3.5"
+ else:
+ # Just default to `gpt-4o` for now, since this seems to be the most recent tokenizer
+ # and we are only using it for estimating token usage
+ fixed_model_name = "gpt-4o"
+ return tiktoken.encoding_for_model(fixed_model_name)
+
+
+def count_openai_tokens(text: str, model_name: str) -> int:
+ return len(get_openai_tokenizer(model_name).encode(text))
+
+
+@contextmanager
+def _openai_exception_manager() -> Iterator[None]:
+ """Simple context manager for parsing OpenAI API exceptions."""
+ try:
+ yield
+ except BadRequestError as e:
+ error_text_match = _OPENAI_COMPLETION_ERROR_PATTERN.search(str(e))
+ if error_text_match is not None:
+ max_prompt_len = int(error_text_match.group(1))
+ prompt_len = int(error_text_match.group(2))
+ logger.debug(
+ "PromptTooLongError max_prompt_len={max_prompt_len} prompt_len={prompt_len}",
+ max_prompt_len=max_prompt_len,
+ prompt_len=prompt_len,
+ )
+ raise PromptTooLongError(prompt_len, max_prompt_len) from e
+ logger.debug("BadAPIRequestError {e}", e=e)
+ raise BadAPIRequestError(str(e)) from e
+ except APIConnectionError as e:
+ logger.debug("Rate limited? Received APIConnectionError {e}", e=e)
+ raise TransientLanguageModelError("APIConnectionError") from e
+ except RateLimitError as e:
+ if e.code == "insufficient_quota":
+ raise
+ logger.debug("Rate limited? {e}", e=e)
+ raise TransientLanguageModelError("RateLimitError") from e
+ except httpx.RemoteProtocolError as e:
+ logger.debug("httpx.RemoteProtocolError {e}", e=e)
+ raise TransientLanguageModelError("httpx.RemoteProtocolError") from e
+ except InternalServerError as e:
+ logger.debug("InternalServerError {e}", e=e)
+ raise TransientLanguageModelError("InternalServerError") from e
+
+
+class OpenAIChatAPI(OpenAICompatibleAPI):
+ model_name: OpenAIModelName = OpenAIModelName.GPT_4O_MINI_2024_07_18
+
+ @field_validator("model_name") # pyre-ignore[56]: pyre doesn't understand pydantic
+ @classmethod
+ def validate_model_name(cls, v: str) -> str:
+ if v not in OPENAI_MODEL_INFO_BY_NAME:
+ raise LanguageModelInvalidModelNameError(v, cls.__name__, list(OPENAI_MODEL_INFO_BY_NAME))
+ return v
+
+ @property
+ def model_info(self) -> ModelInfo:
+ return get_model_info(self.model_name)
+
+ def _get_client(self) -> AsyncOpenAI:
+ api_key = get_secret("OPENAI_API_KEY")
+ if not api_key:
+ raise MissingAPIKeyError("OPENAI_API_KEY environment variable is not set")
+ return AsyncOpenAI( # pyre-ignore[16]: pyre doesn't understand the auto-generated openai._client
+ api_key=api_key
+ )
+
+ async def _call_api(
+ self,
+ prompt: str,
+ params: LanguageModelGenerationParams,
+ network_failure_count: int = 0,
+ ) -> CostedLanguageModelResponse:
+ messages = convert_prompt_to_openai_messages(prompt)
+ with _openai_exception_manager():
+ client = self._get_client()
+
+ is_reasoning_model = is_openai_reasoning_model(self.model_name)
+
+ top_logprobs: NotGiven | int
+ if self.is_using_logprobs:
+ assert not is_reasoning_model, "Logprobs are not supported for reasoning models."
+ top_logprobs = 5
+ else:
+ top_logprobs = NOT_GIVEN
+
+ temperature: NotGiven | float = NOT_GIVEN if is_reasoning_model else params.temperature
+
+ async with _get_capacity_semaphor(self.model_name):
+ api_result = await client.chat.completions.create(
+ model=self.model_name,
+ messages=messages, # type: ignore
+ max_completion_tokens=params.max_tokens,
+ n=params.count,
+ temperature=temperature,
+ stream=False,
+ seed=params.seed,
+ stop=params.stop,
+ presence_penalty=self.presence_penalty,
+ logprobs=self.is_using_logprobs,
+ top_logprobs=top_logprobs,
+ )
+ assert isinstance(api_result, ChatCompletion)
+
+ usage = api_result.usage
+ if usage is not None:
+ completion_tokens = usage.completion_tokens
+ prompt_tokens = usage.prompt_tokens
+ cached_tokens = (
+ usage.prompt_tokens_details.cached_tokens if usage.prompt_tokens_details is not None else 0
+ ) or 0
+ caching_info = CachingInfo(
+ read_from_cache=cached_tokens,
+ provider_specific_data=OpenAICachingInfo(),
+ )
+ else:
+ completion_tokens = 0
+ prompt_tokens = self.count_tokens(prompt)
+ cached_tokens = None
+ caching_info = None
+
+ results: tuple[LanguageModelResponse | LanguageModelResponseWithLogits, ...]
+ if self.is_using_logprobs:
+ results = self._parse_response_with_logprobs(
+ api_result,
+ prompt_tokens=prompt_tokens,
+ stop=params.stop,
+ network_failure_count=network_failure_count,
+ )
+ else:
+ results = self._parse_response_without_logprobs(
+ api_result,
+ prompt_tokens=prompt_tokens,
+ stop=params.stop,
+ network_failure_count=network_failure_count,
+ )
+
+ logger.trace("text: {text}", text=results[0].text)
+ dollars_used = self.calculate_cost(prompt_tokens, completion_tokens)
+ logger.trace("dollars used: {dollars_used}", dollars_used=dollars_used)
+ return CostedLanguageModelResponse(
+ usage=LanguageModelResponseUsage(
+ prompt_tokens_used=prompt_tokens,
+ completion_tokens_used=completion_tokens,
+ dollars_used=dollars_used,
+ caching_info=caching_info,
+ ),
+ responses=tuple(results),
+ )
+
+ async def _get_api_stream(
+ self,
+ prompt: str,
+ params: LanguageModelGenerationParams,
+ ) -> AsyncGenerator[LanguageModelStreamEvent, None]:
+ messages = convert_prompt_to_openai_messages(prompt)
+ with _openai_exception_manager():
+ client = self._get_client()
+
+ is_reasoning_model = is_openai_reasoning_model(self.model_name)
+ temperature: NotGiven | float = NOT_GIVEN if is_reasoning_model else params.temperature
+
+ async with _get_capacity_semaphor(self.model_name):
+ api_result = await client.chat.completions.create(
+ model=self.model_name,
+ messages=messages, # type: ignore
+ max_completion_tokens=params.max_tokens,
+ n=1,
+ temperature=temperature,
+ stop=params.stop,
+ seed=params.seed,
+ stream=True,
+ stream_options={"include_usage": True},
+ presence_penalty=self.presence_penalty,
+ logprobs=False, # not used when streaming
+ top_logprobs=NOT_GIVEN, # only allowed when logprobs=True
+ )
+ assert isinstance(api_result, AsyncStream)
+
+ yield LanguageModelStreamStartEvent()
+
+ usage = None
+ finish_reason: str | None = None
+ async for chunk in api_result:
+ if hasattr(chunk, "usage") and chunk.usage is not None:
+ # final chunk containing usage info after all streaming is done
+ usage = chunk.usage
+ continue
+
+ if chunk.choices:
+ assert len(chunk.choices) == 1, "Currently only count=1 supported for streaming API."
+ data = only(chunk.choices)
+ delta = data.delta.content
+ if delta is not None:
+ yield LanguageModelStreamDeltaEvent(delta=delta)
+ if data.finish_reason:
+ finish_reason = str(data.finish_reason)
+
+ stop_reason = _OPENAI_STOP_REASON_TO_STOP_REASON[str(finish_reason)]
+ # Note, OpenAI API treats end turn and stop sequence the same
+ # Here we assume it is stop sequence if user has specified a stop sequence
+ if params.stop is not None and stop_reason == ResponseStopReason.END_TURN:
+ yield LanguageModelStreamDeltaEvent(delta=params.stop)
+
+ if usage is not None:
+ completion_tokens = usage.completion_tokens
+ prompt_tokens = usage.prompt_tokens
+ dollars_used = self.calculate_cost(prompt_tokens, completion_tokens)
+ cached_tokens = usage.prompt_tokens_details.cached_tokens
+ logger.trace(
+ "Used this many cached read tokens: {cached_tokens}",
+ cached_tokens=cached_tokens,
+ )
+ caching_info = CachingInfo(
+ read_from_cache=cached_tokens,
+ provider_specific_data=OpenAICachingInfo(),
+ )
+ else:
+ completion_tokens = -1
+ prompt_tokens = -1
+ dollars_used = -1
+ caching_info = None
+ logger.trace("dollars used: {dollars_used}", dollars_used=dollars_used)
+
+ yield LanguageModelStreamEndEvent(
+ usage=LanguageModelResponseUsage(
+ prompt_tokens_used=prompt_tokens,
+ completion_tokens_used=completion_tokens,
+ dollars_used=dollars_used,
+ caching_info=caching_info,
+ ),
+ stop_reason=stop_reason,
+ )
+
+ def count_tokens(self, text: str) -> int:
+ return count_openai_tokens(text, self.model_name)
+
+ def _parse_response_without_logprobs(
+ self,
+ response: ChatCompletion,
+ prompt_tokens: int,
+ stop: str | None,
+ network_failure_count: int,
+ ) -> tuple[LanguageModelResponse, ...]:
+ results = []
+ for data in response.choices:
+ assert data.message.content is not None
+ text = data.message.content
+ token_count = self.count_tokens(text) + prompt_tokens
+ stop_reason = _OPENAI_STOP_REASON_TO_STOP_REASON[str(data.finish_reason)]
+ # Note, OpenAI API treats end turn and stop sequence the same
+ # Here we assume it is stop sequence if user has specified a stop sequence
+ if stop is not None and stop_reason == ResponseStopReason.END_TURN:
+ text += stop
+ result = LanguageModelResponse(
+ text=text,
+ token_count=token_count,
+ stop_reason=stop_reason,
+ network_failure_count=network_failure_count,
+ )
+ results.append(result)
+ return tuple(results)
+
+ def _parse_response_with_logprobs(
+ self,
+ response: ChatCompletion,
+ prompt_tokens: int,
+ stop: str | None,
+ network_failure_count: int,
+ ) -> tuple[LanguageModelResponseWithLogits, ...]:
+ results = []
+ for data in response.choices:
+ assert data.message.content is not None
+ logprobs = data.logprobs
+ assert logprobs is not None
+ logprobs_content = logprobs.content
+ assert logprobs_content is not None
+ text = data.message.content
+
+ token_probabilities = []
+ for logprob_token_entry in logprobs_content:
+ top_logprobs = logprob_token_entry.top_logprobs
+ top_entries = [
+ TokenProbability(
+ token=top_logprob_obj.token,
+ log_probability=top_logprob_obj.logprob,
+ is_stop=False,
+ )
+ for top_logprob_obj in top_logprobs
+ ]
+ selected_entry = TokenProbability(
+ token=logprob_token_entry.token,
+ log_probability=logprob_token_entry.logprob,
+ is_stop=False,
+ )
+ if selected_entry in top_entries:
+ top_entries.remove(selected_entry)
+ token_probabilities.append(tuple([selected_entry] + top_entries))
+
+ stop_reason = _OPENAI_STOP_REASON_TO_STOP_REASON[str(data.finish_reason)]
+
+ # Note, OpenAI API treats end turn and stop sequence the same
+ # Here we assume it is stop sequence if user has specified a stop sequence
+ if stop is not None and stop_reason == ResponseStopReason.END_TURN:
+ text += stop
+ token_probabilities.append(
+ tuple(
+ [
+ TokenProbability(
+ token=stop,
+ log_probability=self.stop_token_log_probability,
+ is_stop=True,
+ )
+ ]
+ )
+ )
+ result = LanguageModelResponseWithLogits(
+ text=text,
+ token_probabilities=tuple(token_probabilities),
+ token_count=len(logprobs_content) + prompt_tokens,
+ stop_reason=stop_reason,
+ network_failure_count=network_failure_count,
+ )
+ results.append(result)
+ return tuple(results)
diff --git a/vet/imbue_core/agents/llm_apis/openai_compatible_api.py b/vet/imbue_core/agents/llm_apis/openai_compatible_api.py
@@ -0,0 +1,286 @@
+import math
+from contextlib import contextmanager
+from typing import AsyncGenerator
+from typing import Iterator
+
+import httpx
+from loguru import logger
+from openai import AsyncOpenAI
+from openai import AsyncStream
+from openai import InternalServerError
+from openai import NotGiven
+from openai._exceptions import APIConnectionError
+from openai._exceptions import BadRequestError
+from openai._exceptions import RateLimitError
+from openai.types.chat import ChatCompletion
+
+from vet.imbue_core.agents.llm_apis.api_utils import convert_prompt_to_openai_messages
+from vet.imbue_core.agents.llm_apis.constants import approximate_token_count
+from vet.imbue_core.agents.llm_apis.data_types import CachingInfo
+from vet.imbue_core.agents.llm_apis.data_types import CostedLanguageModelResponse
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelGenerationParams
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelResponse
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelResponseUsage
+from vet.imbue_core.agents.llm_apis.data_types import ResponseStopReason
+from vet.imbue_core.agents.llm_apis.errors import BadAPIRequestError
+from vet.imbue_core.agents.llm_apis.errors import PromptTooLongError
+from vet.imbue_core.agents.llm_apis.errors import TransientLanguageModelError
+from vet.imbue_core.agents.llm_apis.language_model_api import LanguageModelAPI
+from vet.imbue_core.agents.llm_apis.models import ModelInfo
+from vet.imbue_core.agents.llm_apis.openai_data_types import OpenAICachingInfo
+from vet.imbue_core.agents.llm_apis.stream import LanguageModelStreamDeltaEvent
+from vet.imbue_core.agents.llm_apis.stream import LanguageModelStreamEndEvent
+from vet.imbue_core.agents.llm_apis.stream import LanguageModelStreamEvent
+from vet.imbue_core.agents.llm_apis.stream import LanguageModelStreamStartEvent
+from vet.imbue_core.frozen_utils import FrozenDict
+from vet.imbue_core.frozen_utils import FrozenMapping
+from vet.imbue_core.itertools import only
+from vet.imbue_core.secrets_utils import get_secret
+
+_OPENAI_COMPATIBLE_STOP_REASON_TO_STOP_REASON: FrozenMapping[str, ResponseStopReason] = FrozenDict(
+ {
+ "stop": ResponseStopReason.END_TURN,
+ "length": ResponseStopReason.MAX_TOKENS,
+ "tool_calls": ResponseStopReason.TOOL_CALLS,
+ "function_call": ResponseStopReason.FUNCTION_CALL,
+ "content_filter": ResponseStopReason.CONTENT_FILTER,
+ "None": ResponseStopReason.NONE,
+ }
+)
+
+
+# TODO: Should the pre-defined OpenAI model class inherit from this?
+class OpenAICompatibleAPI(LanguageModelAPI):
+ model_name: str
+ base_url: str = "https://api.openai.com/v1"
+ api_key_env: str = "OPENAI_API_KEY"
+ context_window: int | None = None
+ max_output_tokens: int | None = None
+ is_conversational: bool = True
+ presence_penalty: float = 0.0
+ # this shouldn't really ever even be used, but just in case
+ stop_token_log_probability: float = math.log(0.9999)
+
+ @property
+ def model_info(self) -> ModelInfo:
+ if self.context_window is None or self.max_output_tokens is None:
+ raise ValueError("Must provide context_window and max_output_tokens, or subclass must override model_info")
+ return ModelInfo(
+ model_name=self.model_name,
+ cost_per_input_token=0.0,
+ cost_per_output_token=0.0,
+ max_input_tokens=self.context_window,
+ max_output_tokens=self.max_output_tokens,
+ rate_limit_req=None,
+ )
+
+ def _get_client(self) -> AsyncOpenAI:
+ api_key = get_secret(self.api_key_env) if self.api_key_env else ""
+ if not api_key:
+ api_key = "not-required"
+ logger.debug("API key not set, attempting to use API without key.")
+
+ return AsyncOpenAI(
+ api_key=api_key,
+ base_url=self.base_url,
+ )
+
+ @contextmanager
+ def _exception_handler(self, prompt: str) -> Iterator[None]:
+ try:
+ yield
+ except BadRequestError as e:
+ if e.code == "context_length_exceeded":
+ prompt_len = self.count_tokens(prompt)
+ max_prompt_len = self.model_info.max_input_tokens
+ logger.debug(
+ "PromptTooLongError max_prompt_len={max_prompt_len} prompt_len={prompt_len}",
+ max_prompt_len=max_prompt_len,
+ prompt_len=prompt_len,
+ )
+ raise PromptTooLongError(prompt_len, max_prompt_len) from e
+ logger.debug("BadAPIRequestError {e}", e=e)
+ raise BadAPIRequestError(str(e)) from e
+ except APIConnectionError as e:
+ logger.debug("API connection error: {e}", e=e)
+ raise TransientLanguageModelError("APIConnectionError") from e
+ except RateLimitError as e:
+ if e.code == "insufficient_quota":
+ raise
+ logger.debug("Rate limited: {e}", e=e)
+ raise TransientLanguageModelError("RateLimitError") from e
+ except httpx.RemoteProtocolError as e:
+ logger.debug("httpx.RemoteProtocolError {e}", e=e)
+ raise TransientLanguageModelError("httpx.RemoteProtocolError") from e
+ except InternalServerError as e:
+ logger.debug("InternalServerError {e}", e=e)
+ raise TransientLanguageModelError("InternalServerError") from e
+
+ async def _call_api(
+ self,
+ prompt: str,
+ params: LanguageModelGenerationParams,
+ network_failure_count: int = 0,
+ ) -> CostedLanguageModelResponse:
+ messages = convert_prompt_to_openai_messages(prompt)
+
+ with self._exception_handler(prompt):
+ client = self._get_client()
+
+ temperature: NotGiven | float = params.temperature
+
+ api_result = await client.chat.completions.create(
+ model=self.model_name,
+ messages=messages,
+ max_completion_tokens=params.max_tokens,
+ n=params.count,
+ temperature=temperature,
+ stream=False,
+ seed=params.seed,
+ stop=params.stop,
+ presence_penalty=self.presence_penalty,
+ )
+ assert isinstance(api_result, ChatCompletion)
+
+ usage = api_result.usage
+ if usage is not None:
+ completion_tokens = usage.completion_tokens
+ prompt_tokens = usage.prompt_tokens
+ cached_tokens = (
+ usage.prompt_tokens_details.cached_tokens if usage.prompt_tokens_details is not None else 0
+ ) or 0
+ caching_info = CachingInfo(
+ read_from_cache=cached_tokens,
+ provider_specific_data=OpenAICachingInfo(),
+ )
+ else:
+ completion_tokens = 0
+ prompt_tokens = self.count_tokens(prompt)
+ cached_tokens = None
+ caching_info = None
+
+ results = self._parse_response(
+ api_result,
+ prompt_tokens=prompt_tokens,
+ stop=params.stop,
+ network_failure_count=network_failure_count,
+ )
+
+ logger.trace("text: {text}", text=results[0].text)
+ dollars_used = self.calculate_cost(prompt_tokens, completion_tokens)
+ logger.trace("dollars used: {dollars_used}", dollars_used=dollars_used)
+
+ return CostedLanguageModelResponse(
+ usage=LanguageModelResponseUsage(
+ prompt_tokens_used=prompt_tokens,
+ completion_tokens_used=completion_tokens,
+ dollars_used=dollars_used,
+ caching_info=caching_info,
+ ),
+ responses=tuple(results),
+ )
+
+ async def _get_api_stream(
+ self,
+ prompt: str,
+ params: LanguageModelGenerationParams,
+ ) -> AsyncGenerator[LanguageModelStreamEvent, None]:
+ messages = convert_prompt_to_openai_messages(prompt)
+
+ with self._exception_handler(prompt):
+ client = self._get_client()
+
+ temperature: NotGiven | float = params.temperature
+
+ api_result = await client.chat.completions.create(
+ model=self.model_name,
+ messages=messages,
+ max_completion_tokens=params.max_tokens,
+ n=1,
+ temperature=temperature,
+ stop=params.stop,
+ seed=params.seed,
+ stream=True,
+ stream_options={"include_usage": True},
+ presence_penalty=self.presence_penalty,
+ )
+ assert isinstance(api_result, AsyncStream)
+
+ yield LanguageModelStreamStartEvent()
+
+ usage = None
+ finish_reason: str | None = None
+ async for chunk in api_result:
+ if hasattr(chunk, "usage") and chunk.usage is not None:
+ usage = chunk.usage
+ continue
+
+ if chunk.choices:
+ assert len(chunk.choices) == 1, "Currently only count=1 supported for streaming API."
+ data = only(chunk.choices)
+ delta = data.delta.content
+ if delta is not None:
+ yield LanguageModelStreamDeltaEvent(delta=delta)
+ if data.finish_reason:
+ finish_reason = str(data.finish_reason)
+
+ stop_reason = _OPENAI_COMPATIBLE_STOP_REASON_TO_STOP_REASON.get(str(finish_reason), ResponseStopReason.NONE)
+ if params.stop is not None and stop_reason == ResponseStopReason.END_TURN:
+ yield LanguageModelStreamDeltaEvent(delta=params.stop)
+
+ if usage is not None:
+ completion_tokens = usage.completion_tokens
+ prompt_tokens = usage.prompt_tokens
+ dollars_used = self.calculate_cost(prompt_tokens, completion_tokens)
+ cached_tokens = (
+ usage.prompt_tokens_details.cached_tokens if usage.prompt_tokens_details is not None else 0
+ ) or 0
+ caching_info = CachingInfo(
+ read_from_cache=cached_tokens,
+ provider_specific_data=OpenAICachingInfo(),
+ )
+ else:
+ completion_tokens = -1
+ prompt_tokens = -1
+ dollars_used = -1
+ caching_info = None
+ logger.trace("dollars used: {dollars_used}", dollars_used=dollars_used)
+
+ yield LanguageModelStreamEndEvent(
+ usage=LanguageModelResponseUsage(
+ prompt_tokens_used=prompt_tokens,
+ completion_tokens_used=completion_tokens,
+ dollars_used=dollars_used,
+ caching_info=caching_info,
+ ),
+ stop_reason=stop_reason,
+ )
+
+ def count_tokens(self, text: str) -> int:
+ return approximate_token_count(text)
+
+ def _parse_response(
+ self,
+ response: ChatCompletion,
+ prompt_tokens: int,
+ stop: str | None,
+ network_failure_count: int,
+ ) -> tuple[LanguageModelResponse, ...]:
+ results = []
+ for data in response.choices:
+ assert data.message.content is not None
+ text = data.message.content
+ token_count = self.count_tokens(text) + prompt_tokens
+ stop_reason = _OPENAI_COMPATIBLE_STOP_REASON_TO_STOP_REASON.get(
+ str(data.finish_reason), ResponseStopReason.NONE
+ )
+ if stop is not None and stop_reason == ResponseStopReason.END_TURN:
+ text += stop
+ result = LanguageModelResponse(
+ text=text,
+ token_count=token_count,
+ stop_reason=stop_reason,
+ network_failure_count=network_failure_count,
+ )
+ results.append(result)
+ return tuple(results)
diff --git a/vet/imbue_core/agents/llm_apis/openai_data_types.py b/vet/imbue_core/agents/llm_apis/openai_data_types.py
@@ -0,0 +1,13 @@
+from vet.imbue_core.pydantic_serialization import SerializableModel
+
+
+class OpenAIModelInfo(SerializableModel):
+ """Currently there isn't any model info specific to OpenAI"""
+
+ object_type: str = "OpenAIModelInfo"
+
+
+class OpenAICachingInfo(SerializableModel):
+ """Currently there isn't any caching info specific to OpenAI"""
+
+ object_type: str = "OpenAICachingInfo"
diff --git a/vet/imbue_core/agents/llm_apis/stream.py b/vet/imbue_core/agents/llm_apis/stream.py
@@ -0,0 +1,168 @@
+import abc
+import asyncio
+from collections.abc import AsyncIterator
+from contextlib import aclosing
+from typing import Any
+from typing import AsyncGenerator
+from typing import Sequence
+
+import anyio
+
+from vet.imbue_core.agents.llm_apis.data_types import CachedCostedLanguageModelResponse
+from vet.imbue_core.agents.llm_apis.data_types import CostedLanguageModelResponse
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelResponse
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelResponseUsage
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelStreamInputs
+from vet.imbue_core.agents.llm_apis.data_types import ModelResponse
+from vet.imbue_core.agents.llm_apis.data_types import ResponseStopReason
+from vet.imbue_core.agents.primitives.resource_limits import PaymentAuthorization
+from vet.imbue_core.agents.primitives.resource_limits import get_global_resource_limits
+from vet.imbue_core.caching import AsyncCache
+from vet.imbue_core.pydantic_serialization import SerializableModel
+
+
+class LanguageModelStreamStartEvent(SerializableModel):
+ pass
+
+
+class LanguageModelStreamDeltaEvent(SerializableModel):
+ delta: str
+ # TODO add per delta token count (if there is a demand)
+ # TODO add per delta logprobs (if there is a demand)
+
+
+class LanguageModelStreamEndEvent(SerializableModel):
+ usage: LanguageModelResponseUsage
+ stop_reason: ResponseStopReason
+
+
+LanguageModelStreamEvent = LanguageModelStreamStartEvent | LanguageModelStreamDeltaEvent | LanguageModelStreamEndEvent
+
+
+class LanguageModelStreamCallback(abc.ABC, SerializableModel):
+ @abc.abstractmethod
+ async def __call__(self, response: CostedLanguageModelResponse) -> None: ...
+
+
+class UpdateCacheCallback(LanguageModelStreamCallback):
+ key: str
+ cache: AsyncCache[CachedCostedLanguageModelResponse]
+ api_inputs: LanguageModelStreamInputs | None
+
+ async def __call__(self, response: CostedLanguageModelResponse) -> None:
+ async with self.cache:
+ await self.cache.set(
+ self.key,
+ CachedCostedLanguageModelResponse(response=response, inputs=self.api_inputs),
+ )
+
+
+class PromptDebuggingCallback(LanguageModelStreamCallback):
+ prompt: str
+ output_path: anyio.Path
+
+ async def __call__(self, response: CostedLanguageModelResponse) -> None:
+ await self.output_path.write_text(self.prompt + response.responses[0].text)
+
+
+class SettleSpendCallback(LanguageModelStreamCallback):
+ auth: PaymentAuthorization
+
+ async def __call__(self, response: CostedLanguageModelResponse) -> None:
+ dollars_used = response.usage.dollars_used
+ global_resource_limits = get_global_resource_limits()
+ assert global_resource_limits is not None
+ await global_resource_limits.settle_spend(self.auth, dollars_used)
+ return None
+
+
+async def consume_async_iterator(iterator: AsyncIterator[Any]) -> None:
+ async for _ in iterator:
+ ...
+
+
+async def get_cached_response_stream(
+ response: CostedLanguageModelResponse,
+) -> AsyncGenerator[LanguageModelStreamEvent, None]:
+ """Simple stream that return cached response in a single go.
+
+ Implemented here so user get's a consistent interface from stream().
+ """
+ yield LanguageModelStreamStartEvent()
+ yield LanguageModelStreamDeltaEvent(delta=response.responses[0].text)
+ yield LanguageModelStreamEndEvent(usage=response.usage, stop_reason=response.responses[0].stop_reason)
+
+
+class StreamedLanguageModelResponse(ModelResponse):
+ """A stream of LanguageModel API events."""
+
+ text_stream: AsyncIterator[str]
+
+ def __init__(
+ self,
+ # Note event_stream is AsyncGenerator as it supports aclose for better cleanup (unlike AsyncIterator)
+ event_stream: AsyncGenerator[LanguageModelStreamEvent, None],
+ network_failure_count: int,
+ completion_callbacks: Sequence[LanguageModelStreamCallback] = (),
+ ) -> None:
+ # the underlying stream coming from the API
+ self._event_stream = event_stream
+ self.text_stream = self._stream_text()
+ self._final_message_snapshot: LanguageModelResponse | None = None
+
+ self._completion_callbacks = completion_callbacks
+ # this is propogated to final message
+ self._network_failure_count = network_failure_count
+ self.stop_reason: ResponseStopReason | None = None
+
+ async def get_final_message(self) -> LanguageModelResponse:
+ # wait until final message
+ await consume_async_iterator(self._event_stream)
+ assert self._final_message_snapshot is not None
+ return self._final_message_snapshot
+
+ async def __aiter__(self) -> AsyncGenerator[LanguageModelStreamEvent, None]:
+ # iterator of events, with handling of shutdown
+ async with aclosing(self._event_stream) as event_stream:
+ deltas: list[str] = []
+ async for event in event_stream:
+ if isinstance(event, LanguageModelStreamStartEvent):
+ # Need nested if statement here for outer if-elif-else to correctly filter for unknown event types
+ if len(deltas) > 0 or self._final_message_snapshot is not None:
+ raise RuntimeError("Start event should be the first event in stream.")
+ elif isinstance(event, LanguageModelStreamDeltaEvent):
+ deltas.append(event.delta)
+ elif isinstance(event, LanguageModelStreamEndEvent):
+ self.stop_reason = event.stop_reason
+ self._final_message_snapshot = LanguageModelResponse(
+ text="".join(deltas),
+ token_count=(0 if event.usage is None else event.usage.completion_tokens_used),
+ stop_reason=event.stop_reason,
+ network_failure_count=self._network_failure_count,
+ )
+ if self._completion_callbacks is not None:
+ costed_response = CostedLanguageModelResponse(
+ usage=event.usage, responses=(self._final_message_snapshot,)
+ )
+ await asyncio.gather(*[callback(costed_response) for callback in self._completion_callbacks])
+ else:
+ raise ValueError(f"Unknown or Unexpected StreamEvent type {type(event)}.")
+
+ yield event
+
+ async def _stream_text(self) -> AsyncIterator[str]:
+ # iterator of text delta
+ async for event in self:
+ if isinstance(event, LanguageModelStreamDeltaEvent):
+ yield event.delta
+
+ async def __aenter__(self) -> "StreamedLanguageModelResponse":
+ return self
+
+ async def __aexit__(self, *exc: Any) -> None:
+ await self.aclose()
+
+ async def aclose(self) -> None:
+ """Close iterator."""
+ # clean up underlying stream (currently not sure how to do this/if we need to do this)
+ await self._event_stream.aclose()
diff --git a/vet/imbue_core/agents/llm_apis/together_api.py b/vet/imbue_core/agents/llm_apis/together_api.py
@@ -0,0 +1,611 @@
+import asyncio
+import enum
+import math
+from collections import defaultdict
+from contextlib import contextmanager
+from typing import AsyncGenerator
+from typing import Final
+from typing import Iterable
+from typing import Iterator
+from typing import Mapping
+from typing import cast
+
+from loguru import logger
+from pydantic.functional_validators import field_validator
+from together import AsyncTogether
+from together.abstract.api_requestor import APIRequestor
+from together.abstract.api_requestor import AioHTTPSession
+from together.error import APIConnectionError
+from together.error import APIError
+from together.error import AuthenticationError
+from together.error import InvalidRequestError
+from together.error import RateLimitError
+from together.error import ServiceUnavailableError
+from together.together_response import TogetherResponse
+from together.types import TogetherRequest
+from together.types.chat_completions import ChatCompletionResponse
+
+from vet.imbue_core.agents.llm_apis.data_types import CostedLanguageModelResponse
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelGenerationParams
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelResponseUsage
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelResponseWithLogits
+from vet.imbue_core.agents.llm_apis.data_types import ResponseStopReason
+from vet.imbue_core.agents.llm_apis.data_types import TokenProbability
+from vet.imbue_core.agents.llm_apis.errors import BadAPIRequestError
+from vet.imbue_core.agents.llm_apis.errors import LanguageModelInvalidModelNameError
+from vet.imbue_core.agents.llm_apis.errors import MissingAPIKeyError
+from vet.imbue_core.agents.llm_apis.errors import TransientLanguageModelError
+from vet.imbue_core.agents.llm_apis.language_model_api import LanguageModelAPI
+from vet.imbue_core.agents.llm_apis.models import ModelInfo
+from vet.imbue_core.agents.llm_apis.stream import LanguageModelStreamDeltaEvent
+from vet.imbue_core.agents.llm_apis.stream import LanguageModelStreamEndEvent
+from vet.imbue_core.agents.llm_apis.stream import LanguageModelStreamEvent
+from vet.imbue_core.agents.llm_apis.stream import LanguageModelStreamStartEvent
+from vet.imbue_core.frozen_utils import FrozenDict
+from vet.imbue_core.frozen_utils import FrozenMapping
+from vet.imbue_core.itertools import only
+from vet.imbue_core.secrets_utils import get_secret
+
+
+# This function is monkeypatched as the original method does not catch BaseExceptions and asyncio.CancelledErrors are BaseExceptions
+async def arequest(
+ self: APIRequestor,
+ options: TogetherRequest,
+ stream: bool = False,
+ request_timeout: float | tuple[float, float] | None = None,
+) -> tuple[TogetherResponse | AsyncGenerator[TogetherResponse, None], bool, str | None]:
+ ctx = AioHTTPSession()
+ session = await ctx.__aenter__()
+ result = None
+ try:
+ result = await self.arequest_raw(
+ options,
+ session,
+ request_timeout=request_timeout,
+ )
+ resp, got_stream = await self._interpret_async_response(result, stream)
+ except BaseException:
+ # Close the request before exiting session context.
+ if result is not None:
+ result.release()
+ await ctx.__aexit__(None, None, None)
+ raise
+ if got_stream:
+
+ async def wrap_resp() -> AsyncGenerator[TogetherResponse, None]:
+ assert isinstance(resp, AsyncGenerator)
+ try:
+ async for r in resp:
+ yield r
+ finally:
+ # Close the request before exiting session context. Important to do it here
+ # as if stream is not fully exhausted, we need to close the request nevertheless.
+ result.release()
+ await ctx.__aexit__(None, None, None)
+
+ return wrap_resp(), got_stream, self.api_key
+ else:
+ # Close the request before exiting session context.
+ result.release()
+ await ctx.__aexit__(None, None, None)
+ return resp, got_stream, self.api_key
+
+
+APIRequestor.arequest = arequest # pyre-fixme[8]: pyre is confused about this
+
+
+class TogetherAIModelName(enum.StrEnum):
+ GOOGLE_GEMMA_2_27B_IT = "together/google/gemma-2-27b-it"
+ GOOGLE_GEMMA_2_9B_IT = "together/google/gemma-2-9b-it"
+ GOOGLE_GEMMA_2B_IT = "together/google/gemma-2b-it"
+ META_LLAMA_3_2_3B_INSTRUCT_TURBO = "together/meta-llama/Llama-3.2-3B-Instruct-Turbo"
+ META_LLAMA_3_3_70B_INSTRUCT_TURBO = "together/meta-llama/Llama-3.3-70B-Instruct-Turbo"
+ META_LLAMA_3_70B_CHAT_HF = "together/meta-llama/Llama-3-70b-chat-hf"
+ META_LLAMA_3_8B_CHAT_HF = "together/meta-llama/Llama-3-8b-chat-hf"
+ META_LLAMA_3_1_405B_INSTRUCT_TURBO = "together/meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo"
+ META_LLAMA_3_1_70B_INSTRUCT_TURBO = "together/meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo"
+ META_LLAMA_3_1_8B_INSTRUCT_TURBO = "together/meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo"
+ META_LLAMA_3_70B_INSTRUCT_LITE = "together/meta-llama/Meta-Llama-3-70B-Instruct-Lite"
+ META_LLAMA_3_70B_INSTRUCT_TURBO = "together/meta-llama/Meta-Llama-3-70B-Instruct-Turbo"
+ META_LLAMA_3_8B_INSTRUCT_LITE = "together/meta-llama/Meta-Llama-3-8B-Instruct-Lite"
+ META_LLAMA_3_8B_INSTRUCT_TURBO = "together/meta-llama/Meta-Llama-3-8B-Instruct-Turbo"
+ MISTRALAI_MISTRAL_7B_INSTRUCT_V0_1 = "together/mistralai/Mistral-7B-Instruct-v0.1"
+ MISTRALAI_MISTRAL_7B_INSTRUCT_V0_2 = "together/mistralai/Mistral-7B-Instruct-v0.2"
+ MISTRALAI_MISTRAL_7B_INSTRUCT_V0_3 = "together/mistralai/Mistral-7B-Instruct-v0.3"
+ MISTRALAI_MIXTRAL_8X22B_INSTRUCT_V0_1 = "together/mistralai/Mixtral-8x22B-Instruct-v0.1"
+ MISTRALAI_MIXTRAL_8X7B_INSTRUCT_V0_1 = "together/mistralai/Mixtral-8x7B-Instruct-v0.1"
+ NOUSRESEARCH_NOUS_HERMES_2_MIXTRAL_8X7B_DPO = "together/NousResearch/Nous-Hermes-2-Mixtral-8x7B-DPO"
+ DEEPSEEK_R1 = "together/deepseek-ai/DeepSeek-R1"
+ OPENAI_GPT_OSS_20B = "together/openai/gpt-oss-20b"
+ OPENAI_GPT_OSS_120B = "together/openai/gpt-oss-120b"
+ # TOGETHERCOMPUTER_LLAMA_3_8B_CHAT_HF_INT4 = "together/togethercomputer/Llama-3-8b-chat-hf-int4"
+ # TOGETHERCOMPUTER_LLAMA_3_8B_CHAT_HF_INT8 = "together/togethercomputer/Llama-3-8b-chat-hf-int8"
+
+
+# Rate limits for Together AI models based on published API documentation
+# Reference: https://docs.together.ai/docs/rate-limits
+# Using Tier 5 rate limits (6,000 RPM)
+
+TOGETHERAI_MODEL_INFO_BY_NAME: FrozenMapping[TogetherAIModelName, ModelInfo] = FrozenDict(
+ {
+ # ref https://docs.together.ai/docs/chat-models
+ # pricing ref https://www.together.ai/pricing
+ TogetherAIModelName.GOOGLE_GEMMA_2_27B_IT: ModelInfo(
+ model_name=str(TogetherAIModelName.GOOGLE_GEMMA_2_27B_IT),
+ cost_per_input_token=0.8 / 1_000_000,
+ cost_per_output_token=0.8 / 1_000_000,
+ max_input_tokens=8192,
+ max_output_tokens=None,
+ rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
+ ),
+ TogetherAIModelName.GOOGLE_GEMMA_2_9B_IT: ModelInfo(
+ model_name=str(TogetherAIModelName.GOOGLE_GEMMA_2_9B_IT),
+ cost_per_input_token=0.3 / 1_000_000,
+ cost_per_output_token=0.3 / 1_000_000,
+ max_input_tokens=8192,
+ max_output_tokens=None,
+ rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
+ ),
+ TogetherAIModelName.GOOGLE_GEMMA_2B_IT: ModelInfo(
+ model_name=str(TogetherAIModelName.GOOGLE_GEMMA_2B_IT),
+ cost_per_input_token=0.1 / 1_000_000,
+ cost_per_output_token=0.1 / 1_000_000,
+ max_input_tokens=8192,
+ max_output_tokens=None,
+ rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
+ ),
+ TogetherAIModelName.META_LLAMA_3_2_3B_INSTRUCT_TURBO: ModelInfo(
+ model_name=str(TogetherAIModelName.META_LLAMA_3_2_3B_INSTRUCT_TURBO),
+ cost_per_input_token=0.06 / 1_000_000,
+ cost_per_output_token=0.06 / 1_000_000,
+ max_input_tokens=131072,
+ max_output_tokens=None,
+ rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
+ ),
+ TogetherAIModelName.META_LLAMA_3_3_70B_INSTRUCT_TURBO: ModelInfo(
+ model_name=str(TogetherAIModelName.META_LLAMA_3_3_70B_INSTRUCT_TURBO),
+ cost_per_input_token=0.88 / 1_000_000,
+ cost_per_output_token=0.88 / 1_000_000,
+ max_input_tokens=131072,
+ max_output_tokens=None,
+ rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
+ ),
+ TogetherAIModelName.META_LLAMA_3_70B_CHAT_HF: ModelInfo(
+ model_name=str(TogetherAIModelName.META_LLAMA_3_70B_CHAT_HF),
+ cost_per_input_token=0.88 / 1_000_000,
+ cost_per_output_token=0.88 / 1_000_000,
+ max_input_tokens=8192,
+ max_output_tokens=None,
+ rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
+ ),
+ TogetherAIModelName.META_LLAMA_3_8B_CHAT_HF: ModelInfo(
+ model_name=str(TogetherAIModelName.META_LLAMA_3_8B_CHAT_HF),
+ cost_per_input_token=0.2 / 1_000_000,
+ cost_per_output_token=0.2 / 1_000_000,
+ max_input_tokens=8192,
+ max_output_tokens=None,
+ rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
+ ),
+ TogetherAIModelName.META_LLAMA_3_1_405B_INSTRUCT_TURBO: ModelInfo(
+ model_name=str(TogetherAIModelName.META_LLAMA_3_1_405B_INSTRUCT_TURBO),
+ cost_per_input_token=3.5 / 1_000_000,
+ cost_per_output_token=3.5 / 1_000_000,
+ max_input_tokens=130815,
+ max_output_tokens=None,
+ rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
+ ),
+ TogetherAIModelName.META_LLAMA_3_1_70B_INSTRUCT_TURBO: ModelInfo(
+ model_name=str(TogetherAIModelName.META_LLAMA_3_1_70B_INSTRUCT_TURBO),
+ cost_per_input_token=0.88 / 1_000_000,
+ cost_per_output_token=0.88 / 1_000_000,
+ max_input_tokens=131072,
+ max_output_tokens=None,
+ rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
+ ),
+ TogetherAIModelName.META_LLAMA_3_1_8B_INSTRUCT_TURBO: ModelInfo(
+ model_name=str(TogetherAIModelName.META_LLAMA_3_1_8B_INSTRUCT_TURBO),
+ cost_per_input_token=0.18 / 1_000_000,
+ cost_per_output_token=0.18 / 1_000_000,
+ max_input_tokens=131072,
+ max_output_tokens=None,
+ rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
+ ),
+ TogetherAIModelName.META_LLAMA_3_70B_INSTRUCT_LITE: ModelInfo(
+ model_name=str(TogetherAIModelName.META_LLAMA_3_70B_INSTRUCT_LITE),
+ cost_per_input_token=0.54 / 1_000_000,
+ cost_per_output_token=0.54 / 1_000_000,
+ max_input_tokens=8192,
+ max_output_tokens=None,
+ rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
+ ),
+ TogetherAIModelName.META_LLAMA_3_70B_INSTRUCT_TURBO: ModelInfo(
+ model_name=str(TogetherAIModelName.META_LLAMA_3_70B_INSTRUCT_TURBO),
+ cost_per_input_token=0.88 / 1_000_000,
+ cost_per_output_token=0.88 / 1_000_000,
+ max_input_tokens=8192,
+ max_output_tokens=None,
+ rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
+ ),
+ TogetherAIModelName.META_LLAMA_3_8B_INSTRUCT_LITE: ModelInfo(
+ model_name=str(TogetherAIModelName.META_LLAMA_3_8B_INSTRUCT_LITE),
+ cost_per_input_token=0.1 / 1_000_000,
+ cost_per_output_token=0.1 / 1_000_000,
+ max_input_tokens=8192,
+ max_output_tokens=None,
+ rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
+ ),
+ TogetherAIModelName.META_LLAMA_3_8B_INSTRUCT_TURBO: ModelInfo(
+ model_name=str(TogetherAIModelName.META_LLAMA_3_8B_INSTRUCT_TURBO),
+ cost_per_input_token=0.18 / 1_000_000,
+ cost_per_output_token=0.18 / 1_000_000,
+ max_input_tokens=8192,
+ max_output_tokens=None,
+ rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
+ ),
+ TogetherAIModelName.MISTRALAI_MISTRAL_7B_INSTRUCT_V0_1: ModelInfo(
+ model_name=str(TogetherAIModelName.MISTRALAI_MISTRAL_7B_INSTRUCT_V0_1),
+ cost_per_input_token=0.2 / 1_000_000,
+ cost_per_output_token=0.2 / 1_000_000,
+ max_input_tokens=32768,
+ max_output_tokens=None,
+ rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
+ ),
+ TogetherAIModelName.MISTRALAI_MISTRAL_7B_INSTRUCT_V0_2: ModelInfo(
+ model_name=str(TogetherAIModelName.MISTRALAI_MISTRAL_7B_INSTRUCT_V0_2),
+ cost_per_input_token=0.2 / 1_000_000,
+ cost_per_output_token=0.2 / 1_000_000,
+ max_input_tokens=32768,
+ max_output_tokens=None,
+ rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
+ ),
+ TogetherAIModelName.MISTRALAI_MISTRAL_7B_INSTRUCT_V0_3: ModelInfo(
+ model_name=str(TogetherAIModelName.MISTRALAI_MISTRAL_7B_INSTRUCT_V0_3),
+ cost_per_input_token=0.2 / 1_000_000,
+ cost_per_output_token=0.2 / 1_000_000,
+ max_input_tokens=32768,
+ max_output_tokens=None,
+ rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
+ ),
+ TogetherAIModelName.MISTRALAI_MIXTRAL_8X22B_INSTRUCT_V0_1: ModelInfo(
+ model_name=str(TogetherAIModelName.MISTRALAI_MIXTRAL_8X22B_INSTRUCT_V0_1),
+ cost_per_input_token=1.2 / 1_000_000,
+ cost_per_output_token=1.2 / 1_000_000,
+ max_input_tokens=65536,
+ max_output_tokens=None,
+ rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
+ ),
+ TogetherAIModelName.MISTRALAI_MIXTRAL_8X7B_INSTRUCT_V0_1: ModelInfo(
+ model_name=str(TogetherAIModelName.MISTRALAI_MIXTRAL_8X7B_INSTRUCT_V0_1),
+ cost_per_input_token=0.6 / 1_000_000,
+ cost_per_output_token=0.6 / 1_000_000,
+ max_input_tokens=32768,
+ max_output_tokens=None,
+ rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
+ ),
+ TogetherAIModelName.NOUSRESEARCH_NOUS_HERMES_2_MIXTRAL_8X7B_DPO: ModelInfo(
+ model_name=str(TogetherAIModelName.NOUSRESEARCH_NOUS_HERMES_2_MIXTRAL_8X7B_DPO),
+ cost_per_input_token=0.6 / 1_000_000,
+ cost_per_output_token=0.6 / 1_000_000,
+ max_input_tokens=32768,
+ max_output_tokens=None,
+ rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
+ ),
+ TogetherAIModelName.DEEPSEEK_R1: ModelInfo(
+ model_name=str(TogetherAIModelName.DEEPSEEK_R1),
+ cost_per_input_token=3.0 / 1_000_000,
+ cost_per_output_token=7.0 / 1_000_000,
+ max_input_tokens=32768,
+ max_output_tokens=None,
+ rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
+ ),
+ TogetherAIModelName.OPENAI_GPT_OSS_20B: ModelInfo(
+ model_name=str(TogetherAIModelName.OPENAI_GPT_OSS_20B),
+ cost_per_input_token=0.00 / 1_000_000,
+ cost_per_output_token=0.00 / 1_000_000,
+ max_input_tokens=131_072,
+ max_output_tokens=131_072,
+ rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
+ ),
+ TogetherAIModelName.OPENAI_GPT_OSS_120B: ModelInfo(
+ model_name=str(TogetherAIModelName.OPENAI_GPT_OSS_120B),
+ cost_per_input_token=0.00 / 1_000_000,
+ cost_per_output_token=0.00 / 1_000_000,
+ max_input_tokens=131_072,
+ max_output_tokens=131_072,
+ rate_limit_req=6000 / 60, # 6000 RPM = 100.00 RPS
+ ),
+ }
+)
+
+
+def _default_capacity_semaphor() -> asyncio.Semaphore:
+ return asyncio.Semaphore(100)
+
+
+_CAPACITY_SEMAPHOR_BY_MODEL_NAME: Mapping[str, asyncio.Semaphore] = defaultdict(_default_capacity_semaphor)
+
+
+_ROLE_TO_TOGETHERAI_ROLE: Final[FrozenMapping] = FrozenDict(
+ {
+ "HUMAN": "user",
+ "ASSISTANT": "assistant",
+ "SYSTEM": "system",
+ "SYSTEM_CACHED": "system",
+ "USER": "user",
+ "USER_CACHED": "user",
+ }
+)
+
+# ref: https://github.com/togethercomputer/together-python/blob/main/src/together/types/common.py#L13
+_TOGETHERAI_STOP_REASON_TO_STOP_REASON: Final[FrozenMapping[str, ResponseStopReason]] = FrozenDict(
+ {
+ "length": ResponseStopReason.MAX_TOKENS,
+ # This is a little sketchy, we treat them the same since we don't know which models emit stop sequence reasons
+ # Since we don't want to break downstream applications that may require ending in the stop sequence
+ # This is similar to how openai models are treated
+ "stop": ResponseStopReason.END_TURN,
+ "eos": ResponseStopReason.END_TURN,
+ "tool_calls": ResponseStopReason.TOOL_CALLS,
+ "error": ResponseStopReason.ERROR,
+ "None": ResponseStopReason.NONE,
+ }
+)
+
+
+def convert_prompt_to_together_messages(prompt: str) -> list[dict[str, str]]:
+ prompt = prompt.lstrip()
+ assert prompt.endswith("\n[ROLE=ASSISTANT]\n"), "prompt must end with [ROLE=ASSISTANT], prompt=\n" + prompt
+ prompt = "".join(prompt.rsplit("\n[ROLE=ASSISTANT]\n", 1))
+ assert prompt.startswith("[ROLE=")
+ prompt = prompt.replace("[ROLE=", "", 1)
+ chunks = prompt.split("\n[ROLE=")
+ messages: list[dict[str, str]] = []
+ for chunk in chunks:
+ lines = chunk.split("\n")
+ role = lines[0].strip().rstrip("]")
+ assert role in (
+ "HUMAN",
+ "ASSISTANT",
+ "USER",
+ "SYSTEM",
+ "SYSTEM_CACHED",
+ "USER_CACHED",
+ ), f"Unknown role {role} in prompt {prompt}"
+ lines.pop(0)
+ if len(messages) > 0:
+ messages[-1]["content"] = messages[-1]["content"] + "\n"
+ messages.append({"role": _ROLE_TO_TOGETHERAI_ROLE[role], "content": "\n".join(lines)})
+ return messages
+
+
+@contextmanager
+def _together_exception_manager() -> Iterator[None]:
+ """Simple context manager for parsing TogetherAI API exceptions."""
+ # ref https://github.com/togethercomputer/together-python/blob/main/src/together/abstract/api_requestor.py#L332
+ try:
+ yield
+ except RateLimitError as e:
+ logger.info("Rate limited? {}", e)
+ raise TransientLanguageModelError("RateLimitError") from e
+ except InvalidRequestError as e:
+ logger.info("BadAPIRequestError {}", e)
+ raise BadAPIRequestError(str(e)) from e
+ except AuthenticationError as e:
+ logger.info("API Authentication error {}", e)
+ raise
+ except APIError as e:
+ logger.info("Received APIError {}", e)
+ raise
+ except ServiceUnavailableError as e:
+ logger.info("Received ServiceUnavailableError {}", e)
+ raise TransientLanguageModelError("ServiceUnavailableError") from e
+ except APIConnectionError as e:
+ logger.info("Received APIConnectionError {}", e)
+ raise TransientLanguageModelError("APIConnectionError") from e
+ # Note, the together python SDK uses aiohttp under the hood
+ # but takes care of parsing the main aiohttp exceptions into together API exceptions
+ # ref https://github.com/togethercomputer/together-python/blob/main/src/together/abstract/api_requestor.py#L554
+
+
+class TogetherAPI(LanguageModelAPI):
+ model_name: TogetherAIModelName = TogetherAIModelName.META_LLAMA_3_1_8B_INSTRUCT_TURBO
+ is_conversational: bool = True
+ presence_penalty: float = 0.0
+ # this shouldn't really ever even be used, but just in case
+ stop_token_log_probability: float = math.log(0.9999)
+
+ @field_validator("model_name") # pyre-ignore[56]: pyre doesn't understand pydantic
+ @classmethod
+ def validate_model_name(cls, v: str) -> str:
+ if v not in TOGETHERAI_MODEL_INFO_BY_NAME:
+ raise LanguageModelInvalidModelNameError(v, cls.__name__, list(TOGETHERAI_MODEL_INFO_BY_NAME))
+ return v
+
+ @property
+ def model_info(self) -> ModelInfo:
+ return TOGETHERAI_MODEL_INFO_BY_NAME[self.model_name]
+
+ @property
+ def external_model_name(self) -> str:
+ return self.model_name.replace("together/", "")
+
+ def _get_client(self) -> AsyncTogether:
+ api_key = get_secret("TOGETHER_API_KEY")
+ if not api_key:
+ raise MissingAPIKeyError("TOGETHER_API_KEY environment variable is not set")
+ return AsyncTogether(api_key=api_key)
+
+ async def _call_api(
+ self,
+ prompt: str,
+ params: LanguageModelGenerationParams,
+ network_failure_count: int = 0,
+ ) -> CostedLanguageModelResponse:
+ if params.max_tokens is None:
+ logger.debug(
+ "Togetherai API breaks if `max_tokens` not specified. Defaulting to `max_tokens=512`, make sure to specify this if you want something different."
+ )
+ params.max_tokens = 512
+
+ with _together_exception_manager():
+ messages = convert_prompt_to_together_messages(prompt)
+ client = self._get_client()
+ async with _CAPACITY_SEMAPHOR_BY_MODEL_NAME[self.model_name]:
+ # ref: https://github.com/togethercomputer/together-python/blob/main/src/together/resources/chat/completions.py#L153
+ api_result = await client.chat.completions.create(
+ model=self.external_model_name,
+ messages=messages,
+ max_tokens=params.max_tokens,
+ stop=[params.stop] if params.stop else None,
+ temperature=params.temperature,
+ top_k=5,
+ presence_penalty=self.presence_penalty,
+ stream=False,
+ logprobs=True,
+ n=params.count,
+ # currently don't specify this since tokenizer may change between models
+ logit_bias=None,
+ )
+ assert isinstance(api_result, ChatCompletionResponse)
+
+ results = []
+ choices = api_result.choices
+ assert choices is not None
+ for data in choices:
+ message = data.message
+ assert message is not None
+ text = message.content
+ assert text is not None
+ assert isinstance(text, str) # TODO: this is suspicious
+
+ # TogetherAI only provides the logprob for the selected token
+ logprobs = data.logprobs
+ assert logprobs is not None
+ tokens = logprobs.tokens
+ token_logprobs = logprobs.token_logprobs
+ assert tokens is not None and token_logprobs is not None
+ assert all(token is not None for token in tokens)
+ assert all(logprob is not None for logprob in token_logprobs)
+ tokens = cast(Iterable[str], tokens)
+ token_logprobs = cast(Iterable[float], token_logprobs)
+ token_probabilities = [
+ (TokenProbability(token=token, log_probability=logprob, is_stop=False),)
+ for token, logprob in zip(tokens, token_logprobs)
+ ]
+
+ if data.finish_reason:
+ stop_reason = _TOGETHERAI_STOP_REASON_TO_STOP_REASON[data.finish_reason.value]
+ else:
+ stop_reason = ResponseStopReason.NONE
+ stop = params.stop
+ if stop is not None and stop_reason == ResponseStopReason.END_TURN:
+ text += stop
+ token_probabilities.append(
+ (
+ TokenProbability(
+ token=stop,
+ log_probability=self.stop_token_log_probability,
+ is_stop=True,
+ ),
+ )
+ )
+ result = LanguageModelResponseWithLogits(
+ text=text,
+ token_probabilities=tuple(token_probabilities),
+ token_count=len(token_probabilities),
+ stop_reason=stop_reason,
+ network_failure_count=network_failure_count,
+ )
+ results.append(result)
+
+ logger.trace("text: " + results[0].text)
+ if api_result.usage is not None:
+ completion_tokens = api_result.usage.completion_tokens
+ prompt_tokens = api_result.usage.prompt_tokens
+ else:
+ completion_tokens = 0
+ prompt_tokens = 0
+ dollars_used = self.calculate_cost(prompt_tokens, completion_tokens)
+ logger.trace("dollars used: {}", dollars_used)
+ return CostedLanguageModelResponse(
+ usage=LanguageModelResponseUsage(
+ prompt_tokens_used=prompt_tokens,
+ completion_tokens_used=completion_tokens,
+ dollars_used=dollars_used,
+ ),
+ responses=tuple(results),
+ )
+
+ async def _get_api_stream(
+ self,
+ prompt: str,
+ params: LanguageModelGenerationParams,
+ ) -> AsyncGenerator[LanguageModelStreamEvent, None]:
+ if params.max_tokens is None:
+ logger.debug(
+ "Togetherai API breaks if `max_tokens` not specified. Defaulting to `max_tokens=512`, make sure to specify this if you want something different."
+ )
+ params.max_tokens = 512
+
+ with _together_exception_manager():
+ messages = convert_prompt_to_together_messages(prompt)
+ client = self._get_client()
+ async with _CAPACITY_SEMAPHOR_BY_MODEL_NAME[self.model_name]:
+ # ref: https://github.com/togethercomputer/together-python/blob/main/src/together/resources/chat/completions.py#L153
+ api_result = await client.chat.completions.create(
+ model=self.external_model_name,
+ messages=messages,
+ max_tokens=params.max_tokens,
+ stop=[params.stop] if params.stop else None,
+ temperature=params.temperature,
+ top_k=5,
+ presence_penalty=self.presence_penalty,
+ stream=True,
+ # currently we don't support logprobs with streaming
+ logprobs=False,
+ n=1,
+ # currently don't specify this since tokenizer may change between models
+ logit_bias=None,
+ )
+ assert isinstance(api_result, AsyncGenerator)
+
+ yield LanguageModelStreamStartEvent()
+
+ usage = None
+ finish_reason: str | None = None
+ async for chunk in api_result:
+ if chunk.usage:
+ usage = chunk.usage
+
+ if chunk.finish_reason:
+ finish_reason = chunk.finish_reason.value
+
+ chunk_choices = chunk.choices
+ if chunk_choices:
+ assert len(chunk_choices) == 1, "Currently only count=1 supported for streaming API."
+ delta = only(chunk_choices).delta
+ if delta and delta.content:
+ yield LanguageModelStreamDeltaEvent(delta=delta.content)
+
+ stop_reason = _TOGETHERAI_STOP_REASON_TO_STOP_REASON[str(finish_reason)]
+
+ if params.stop is not None and stop_reason == ResponseStopReason.END_TURN:
+ yield LanguageModelStreamDeltaEvent(delta=params.stop)
+
+ if usage is not None:
+ completion_tokens = usage.completion_tokens
+ prompt_tokens = usage.prompt_tokens
+ else:
+ completion_tokens = 0
+ prompt_tokens = 0
+ dollars_used = self.calculate_cost(prompt_tokens, completion_tokens)
+ logger.trace("dollars used: {}", dollars_used)
+
+ yield LanguageModelStreamEndEvent(
+ usage=LanguageModelResponseUsage(
+ prompt_tokens_used=prompt_tokens,
+ completion_tokens_used=completion_tokens,
+ dollars_used=dollars_used,
+ ),
+ stop_reason=stop_reason,
+ )
diff --git a/vet/imbue_core/agents/llm_apis/union_data_types.py b/vet/imbue_core/agents/llm_apis/union_data_types.py
@@ -0,0 +1,20 @@
+from typing import Annotated
+
+from pydantic import Tag
+
+from vet.imbue_core.agents.llm_apis.anthropic_data_types import AnthropicCachingInfo
+from vet.imbue_core.agents.llm_apis.anthropic_data_types import AnthropicModelInfo
+from vet.imbue_core.agents.llm_apis.openai_data_types import OpenAICachingInfo
+from vet.imbue_core.agents.llm_apis.openai_data_types import OpenAIModelInfo
+from vet.imbue_core.pydantic_serialization import build_discriminator
+
+ProviderSpecificModelInfoUnion = Annotated[
+ Annotated[AnthropicModelInfo, Tag("AnthropicModelInfo")] | Annotated[OpenAIModelInfo, Tag("OpenAIModelInfo")],
+ build_discriminator(),
+]
+
+ProviderSpecificCachingInfoUnion = Annotated[
+ Annotated[AnthropicCachingInfo, Tag("AnthropicCachingInfo")]
+ | Annotated[OpenAICachingInfo, Tag("OpenAICachingInfo")],
+ build_discriminator(),
+]
diff --git a/imbue_core/imbue_core/agents/primitives/errors.py b/vet/imbue_core/agents/primitives/errors.py
diff --git a/vet/imbue_core/agents/primitives/resource_limits.py b/vet/imbue_core/agents/primitives/resource_limits.py
@@ -0,0 +1,485 @@
+import asyncio
+import datetime
+import os
+from asyncio import CancelledError
+from asyncio import Task
+from asyncio import TaskGroup
+from typing import Any
+from typing import Callable
+from typing import Coroutine
+from typing import Optional
+from uuid import uuid4
+
+import attr
+from loguru import logger
+
+from vet.imbue_core.agents.primitives.errors import DollarLimitExceeded
+from vet.imbue_core.agents.primitives.errors import MaximumSpendExceeded
+from vet.imbue_core.async_monkey_patches import safe_cancel
+from vet.imbue_core.itertools import first
+from vet.imbue_core.serialization_types import Serializable
+from vet.imbue_core.time_utils import get_current_time
+
+# TODO: someday in the future we can be smarter about this...
+_AUTH_PAYMENT_TIMEOUT_SECONDS = 60 * 60 * 24
+_ONE_HOUR_IN_SECONDS = 60 * 60
+
+
+class InvalidResourceLimitsError(Exception):
+ pass
+
+
+class AuthorizationInvalidated(Exception):
+ pass
+
+
+@attr.s(auto_attribs=True, frozen=True)
+class PaymentAuthorization:
+ dollars: float
+ authorization_id: str
+ authorized_at: datetime.datetime
+
+
+@attr.s(auto_attribs=True, frozen=True)
+class ResourceLimitState(Serializable):
+ hard_cap_dollars: float
+ hard_cap_seconds: float
+ warn_cap_dollars: float
+ warn_cap_seconds: float
+ dollars_per_hour: float | None
+
+ @classmethod
+ def build_for_increase(
+ cls,
+ hard_cap_dollars: float = 0.001,
+ hard_cap_seconds: float = 0.001,
+ warn_cap_dollars: float = 0.001,
+ warn_cap_seconds: float = 0.001,
+ dollars_per_hour: float | None = None,
+ ) -> "ResourceLimitState":
+ return cls(
+ hard_cap_dollars=hard_cap_dollars,
+ hard_cap_seconds=hard_cap_seconds,
+ warn_cap_dollars=warn_cap_dollars,
+ warn_cap_seconds=warn_cap_seconds,
+ dollars_per_hour=dollars_per_hour,
+ )
+
+
+def _float_or_none(value: str | None) -> float | None:
+ if value is None:
+ return None
+ return float(value)
+
+
+@attr.s(auto_attribs=True)
+class ResourceLimits:
+ hard_cap_dollars: float
+ hard_cap_seconds: float
+ warn_cap_dollars: float
+ warn_cap_seconds: float
+ # note that setting this effectively caps the size of any given authorization request to this quantity as well
+ # (since it is impossible to spend less than $X per hour if you are spending $X+1 in total)
+ # in such a case, MaximumSpendExceeded will be raised
+ dollars_per_hour: float | None = None
+ parent_limits: Optional["ResourceLimits"] = None
+ dollars_spent: float = 0.0
+ save_spend_callback: Callable[[float], Coroutine[Any, Any, None]] | None = None
+ open_authorizations: dict[str, PaymentAuthorization] = attr.ib(factory=dict)
+ recent_spend_events: list[PaymentAuthorization] = attr.ib(factory=list)
+ # ensure that only a single spend is being authorized at once
+ spend_lock: asyncio.Lock = attr.ib(factory=asyncio.Lock)
+ # prevent us from mutating our state from multiple coroutines at once
+ state_lock: asyncio.Lock = attr.ib(factory=asyncio.Lock)
+ # triggered when the limits are updated
+ limits_updated_event: asyncio.Event = attr.ib(factory=asyncio.Event)
+ # triggered when a settlement happens
+ next_settlement_event: asyncio.Event = attr.ib(factory=asyncio.Event)
+
+ @classmethod
+ def build(
+ cls,
+ *,
+ max_dollars: float | None = None,
+ max_seconds: float | None = None,
+ warn_fraction: float | None = None,
+ dollars_per_hour: float | None = None,
+ ) -> "ResourceLimits":
+ if max_dollars is None and "DEFAULT_MAX_HAMMER_DOLLARS" in os.environ:
+ max_dollars = _float_or_none(os.getenv("DEFAULT_MAX_HAMMER_DOLLARS"))
+ assert max_dollars != float("inf"), "max_dollars must be finite"
+ if max_dollars is None:
+ max_dollars = 0.0
+ assert max_dollars >= 0, "max_dollars must be non-negative"
+ if max_seconds is None and "DEFAULT_MAX_HAMMER_SECONDS" in os.environ:
+ max_seconds = _float_or_none(os.getenv("DEFAULT_MAX_HAMMER_SECONDS"))
+ if max_seconds is None:
+ max_seconds = float("inf")
+ assert max_seconds >= 0, "max_seconds must be non-negative"
+ if warn_fraction is None:
+ # TODO: is it DEFAULT_WARN_FRACTION or DEFAULT_HAMMER_WARN_FRACTION?
+ if "DEFAULT_WARN_FRACTION" in os.environ:
+ warn_fraction = _float_or_none(os.getenv("DEFAULT_HAMMER_WARN_FRACTION"))
+ assert warn_fraction is not None
+ else:
+ warn_fraction = 0.25
+ if dollars_per_hour is None and "DEFAULT_DOLLARS_PER_HOUR" in os.environ:
+ dollars_per_hour = _float_or_none(os.getenv("DEFAULT_DOLLARS_PER_HOUR"))
+ result = cls(
+ hard_cap_dollars=max_dollars,
+ hard_cap_seconds=max_seconds,
+ warn_cap_dollars=max_dollars * warn_fraction,
+ warn_cap_seconds=max_seconds * warn_fraction,
+ dollars_per_hour=dollars_per_hour,
+ )
+ # check that we're not currently in a hammer, otherwise should be calling create_restricted_limits instead
+ try:
+ if get_global_resource_limits() is not None:
+ # If you encounter this, it probably means you're trying to mix hammer and non-hammer resource limiting.
+ # For simplicity and correctness, try to avoid that.
+ raise InvalidResourceLimitsError(
+ "Should not create a new ResourceLimits while global limits are in place. Instead, call create_restricted_limits"
+ )
+ except RuntimeError:
+ pass
+ return result
+
+ def create_restricted_limits(
+ self,
+ *,
+ max_dollars: float | None = None,
+ max_seconds: float | None = None,
+ warn_fraction: float | None = None,
+ hard_cap_dollars: float | None = None,
+ hard_cap_seconds: float | None = None,
+ warn_cap_dollars: float | None = None,
+ warn_cap_seconds: float | None = None,
+ dollars_per_hour: float | None = None,
+ ) -> "ResourceLimits":
+ if max_dollars is not None:
+ assert hard_cap_dollars is None, "Cannot specify both max_dollars and hard_cap_dollars"
+ hard_cap_dollars = max_dollars
+ if warn_fraction is not None:
+ assert warn_cap_dollars is None, "Cannot specify both warn_fraction and warn_cap_dollars"
+ warn_cap_dollars = max_dollars * warn_fraction
+
+ if max_seconds is not None:
+ assert hard_cap_seconds is None, "Cannot specify both max_seconds and hard_cap_seconds"
+ hard_cap_seconds = max_seconds
+ if warn_fraction is not None:
+ assert warn_cap_seconds is None, "Cannot specify both warn_fraction and warn_cap_seconds"
+ warn_cap_seconds = max_seconds * warn_fraction
+
+ if hard_cap_dollars is not None:
+ hard_cap_dollars = min(hard_cap_dollars, self.hard_cap_dollars)
+ if hard_cap_seconds is not None:
+ hard_cap_seconds = min(hard_cap_seconds, self.hard_cap_seconds)
+ if warn_cap_dollars is not None:
+ warn_cap_dollars = min(warn_cap_dollars, self.warn_cap_dollars)
+ if warn_cap_seconds is not None:
+ warn_cap_seconds = min(warn_cap_seconds, self.warn_cap_seconds)
+ if dollars_per_hour is not None and self.dollars_per_hour is not None:
+ dollars_per_hour = min(dollars_per_hour, self.dollars_per_hour)
+ return ResourceLimits(
+ hard_cap_dollars=hard_cap_dollars or self.hard_cap_dollars,
+ hard_cap_seconds=hard_cap_seconds or self.hard_cap_seconds,
+ warn_cap_dollars=warn_cap_dollars or self.warn_cap_dollars,
+ warn_cap_seconds=warn_cap_seconds or self.warn_cap_seconds,
+ dollars_per_hour=dollars_per_hour or self.dollars_per_hour,
+ parent_limits=self,
+ )
+
+ def _get_excessive_spend_message(self, dollars: float, debug_info: Any = None) -> str:
+ msg = f"Tried to spend {dollars} but only {self.hard_cap_dollars - self.dollars_spent} left (of {self.hard_cap_dollars})."
+ if self.hard_cap_dollars == 0:
+ msg += " You might want to set DEFAULT_MAX_HAMMER_DOLLARS to something non-zero"
+ if debug_info is not None:
+ msg += f"\nDebug info: {debug_info}"
+ return msg
+
+ async def authorize_spend(self, dollars: float, debug_info: Any | None = None) -> PaymentAuthorization:
+ # note that we purposefully lock here, even though we are potentially waiting inside the loop below.
+ # the reason for this is that otherwise a large transaction could be starved by a series of smaller transactions
+ # this is annoying to reason about, so this makes it FIFO instead (though potentially at the cost of having to
+ # wait for a while if you are near the limit and there are smaller transactions that could have made it through)
+ async with self.spend_lock:
+ await self._clear_old_authorizations()
+
+ if self.dollars_spent + dollars > self.hard_cap_dollars:
+ raise DollarLimitExceeded(self._get_excessive_spend_message(dollars, debug_info))
+
+ # if we have outstanding authorizations that mean that this transaction would put us over the limit, wait
+ while (await self.get_dollars_authorized_and_spent()) + dollars > self.hard_cap_dollars and (
+ await self.get_dollars_currently_authorized()
+ ) > 0:
+ await self.next_settlement_event.wait()
+
+ # now that some authorizations have settled and we're done waiting, have to check again if this would put us over
+ if self.dollars_spent + dollars > self.hard_cap_dollars:
+ raise DollarLimitExceeded(self._get_excessive_spend_message(dollars))
+
+ dollars_per_hour = self.dollars_per_hour
+ if dollars_per_hour is not None:
+ if dollars > dollars_per_hour:
+ raise MaximumSpendExceeded(
+ f"Tried to spend ${dollars} but only ${dollars_per_hour} / hr allowed, which caps the total spend"
+ )
+
+ # wait until the spend rate is low enough
+ while (await self.get_dollars_authorized_and_spent_in_the_last_hour()) + dollars > dollars_per_hour:
+ oldest_event = first(
+ sorted(
+ [x for x in self.recent_spend_events],
+ key=lambda x: x.authorized_at,
+ )
+ )
+ if oldest_event is None:
+ break
+ time_since_oldest_event = (get_current_time() - oldest_event.authorized_at).total_seconds()
+ time_until_next_event_expires = _ONE_HOUR_IN_SECONDS - time_since_oldest_event
+ logger.debug(
+ f"Waiting until spend rate has subsided (currently at {(await self.get_dollars_authorized_and_spent_in_the_last_hour())} / hr)"
+ )
+ waiting_task = asyncio.create_task(self._wait_until_updated())
+ try:
+ await asyncio.wait_for(waiting_task, timeout=time_until_next_event_expires + 0.01)
+ except TimeoutError:
+ pass
+ await self._clear_old_authorizations()
+
+ assert (await self.get_dollars_authorized_and_spent_in_the_last_hour()) + dollars <= dollars_per_hour
+
+ async with self.state_lock:
+ if self.parent_limits is None:
+ auth = PaymentAuthorization(
+ dollars=dollars,
+ authorization_id=uuid4().hex,
+ authorized_at=get_current_time(),
+ )
+ else:
+ auth = await self.parent_limits.authorize_spend(dollars)
+ self.open_authorizations[auth.authorization_id] = auth
+ return auth
+
+ async def settle_spend(self, authorization: PaymentAuthorization, dollars: float) -> None:
+ async with self.state_lock:
+ await self._clear_old_authorizations(_is_already_locked=True)
+
+ if self.parent_limits is not None:
+ await self.parent_limits.settle_spend(authorization, dollars)
+
+ is_threshold_exceeded_by_this_transaction = (
+ self.dollars_spent < self.warn_cap_dollars <= self.dollars_spent + dollars
+ )
+
+ if authorization.authorization_id not in self.open_authorizations:
+ raise AuthorizationInvalidated(
+ f"Authorization {authorization.authorization_id} has timed out or already been settled"
+ )
+ del self.open_authorizations[authorization.authorization_id]
+ self.recent_spend_events.append(authorization)
+ self.dollars_spent += dollars
+ assert self.save_spend_callback is not None, "Should have been initialized by now"
+ await self.save_spend_callback(self.dollars_spent)
+
+ # notify anything waiting on the next settlement
+ self.next_settlement_event.set()
+ self.next_settlement_event.clear()
+
+ logger.trace(
+ "Settled spend of {}, remaining: {}",
+ dollars,
+ self.hard_cap_dollars - self.dollars_spent,
+ )
+
+ if is_threshold_exceeded_by_this_transaction:
+ await self._warn(f"Spent ${self.dollars_spent} already (will be stopped at ${self.hard_cap_dollars})")
+
+ # TODO: make a more configurable warning system, right now just logs
+ async def _warn(self, message: str) -> None:
+ logger.warning(message)
+
+ async def _clear_old_authorizations(self, _is_already_locked: bool = False) -> None:
+ if not _is_already_locked:
+ await self.state_lock.acquire()
+ try:
+ now = get_current_time()
+ self.open_authorizations = {
+ k: v
+ for k, v in self.open_authorizations.items()
+ if (now - v.authorized_at).total_seconds() < _AUTH_PAYMENT_TIMEOUT_SECONDS
+ }
+ self.recent_spend_events = [
+ x for x in self.recent_spend_events if (now - x.authorized_at).total_seconds() < _ONE_HOUR_IN_SECONDS
+ ]
+ finally:
+ if not _is_already_locked:
+ self.state_lock.release()
+
+ async def get_dollars_currently_authorized(self) -> float:
+ async with self.state_lock:
+ return sum(x.dollars for x in self.open_authorizations.values())
+
+ async def get_dollars_authorized_and_spent(self) -> float:
+ async with self.state_lock:
+ dollars_currently_authorized = float(sum(x.dollars for x in self.open_authorizations.values()))
+ return dollars_currently_authorized + self.dollars_spent
+
+ async def get_dollars_authorized_and_spent_in_the_last_hour(self) -> float:
+ async with self.state_lock:
+ dollars_currently_authorized = float(sum(x.dollars for x in self.open_authorizations.values()))
+ return dollars_currently_authorized + sum(x.dollars for x in self.recent_spend_events)
+
+ async def bump_limits(self, limits: ResourceLimitState) -> ResourceLimitState:
+ """
+ Can only raise limits. This makes it easier for hammers to reason about how much they will be able to spend.
+
+ If we were to allow reducing limits, we'd need to be quite careful to update existing timers, and to check
+ conditions at the end of the wait loop in authorize_spend as well.
+ """
+ if self.parent_limits is None:
+ assert limits.hard_cap_dollars < float("inf"), "Cannot unlimit spend for the top-level hammer"
+
+ async with self.state_lock:
+ self.hard_cap_dollars = max(self.hard_cap_dollars, limits.hard_cap_dollars)
+ self.hard_cap_seconds = max(self.hard_cap_seconds, limits.hard_cap_seconds)
+ self.warn_cap_dollars = max(self.warn_cap_dollars, limits.warn_cap_dollars)
+ self.warn_cap_seconds = max(self.warn_cap_seconds, limits.warn_cap_seconds)
+ if self.dollars_per_hour is None:
+ self.dollars_per_hour = limits.dollars_per_hour
+ else:
+ if limits.dollars_per_hour is not None and limits.dollars_per_hour > self.dollars_per_hour:
+ self.dollars_per_hour = limits.dollars_per_hour
+
+ # notify anything waiting in case we just bumped what they were waiting on
+ if self.dollars_per_hour and limits.dollars_per_hour is not None:
+ self.limits_updated_event.set()
+ self.limits_updated_event.clear()
+
+ return ResourceLimitState(
+ hard_cap_dollars=self.hard_cap_dollars,
+ hard_cap_seconds=self.hard_cap_seconds,
+ warn_cap_dollars=self.warn_cap_dollars,
+ warn_cap_seconds=self.warn_cap_seconds,
+ dollars_per_hour=self.dollars_per_hour,
+ )
+
+ def resume(self, dollars: float, limits: ResourceLimitState) -> None:
+ self.hard_cap_dollars = limits.hard_cap_dollars
+ self.hard_cap_seconds = limits.hard_cap_seconds
+ self.warn_cap_dollars = limits.warn_cap_dollars
+ self.warn_cap_seconds = limits.warn_cap_seconds
+ self.dollars_per_hour = limits.dollars_per_hour
+ self.dollars_spent = dollars
+
+ if self.dollars_spent > self.hard_cap_dollars:
+ raise DollarLimitExceeded(
+ f"Have already spent {self.dollars_spent} dollars (more than the hard cap of {self.hard_cap_dollars})"
+ )
+
+ async def _wait_until_updated(self) -> None:
+ await self.limits_updated_event.wait()
+
+
+@attr.s(auto_attribs=True)
+class HammerTimer:
+ limits: ResourceLimits
+ timer_started_at: datetime.datetime
+ timer_task: Task | None = None
+ is_timeout_warning_issued: bool = False
+
+ # TODO: need to save whether or not we warned about the time so that we dont warn again when resuming
+ async def on_hammer_started(self, task_group: TaskGroup, callback: Callable[[], Coroutine[Any, Any, None]]) -> None:
+ seconds_ago = (get_current_time() - self.timer_started_at).total_seconds()
+ possible_wait_times = [x - seconds_ago for x in [self.limits.hard_cap_seconds, self.limits.warn_cap_seconds]]
+ positive_wait_times = [x for x in possible_wait_times if x > 0]
+ if len(positive_wait_times) == 0:
+ await callback()
+ return
+ time_until_limit_check = min(positive_wait_times)
+ self.timer_task = task_group.create_task(self._timeout_after(time_until_limit_check, callback))
+
+ async def on_hammer_stopped(self) -> None:
+ # cancel the timer (if still running
+ timer_task = self.timer_task
+ if timer_task:
+ safe_cancel(timer_task)
+ try:
+ await timer_task
+ except CancelledError:
+ pass
+ self.timer_task = None
+
+ async def _timeout_after(self, seconds: float, callback: Callable[[], Coroutine[Any, Any, None]]) -> None:
+ while True:
+ await asyncio.sleep(seconds)
+
+ # re-schedule ourselves for the next check if the limits have been updated, or we were just here for a warning
+ time_since_started = (get_current_time() - self.timer_started_at).total_seconds()
+ if time_since_started < self.limits.hard_cap_seconds:
+ # emit a warning if necessary
+ if time_since_started > self.limits.warn_cap_seconds and not self.is_timeout_warning_issued:
+ self.is_timeout_warning_issued = True
+ await self.limits._warn(
+ f"Taking longer than expected ({seconds} sec so far, will be killed at {self.limits.hard_cap_seconds}"
+ )
+ # figure out how long to sleep for
+ seconds = min(
+ [
+ self.limits.hard_cap_seconds - time_since_started,
+ self.limits.warn_cap_seconds - time_since_started,
+ ]
+ )
+ continue
+
+ # if we're still here, we've timed out
+ await callback()
+ return
+
+
+# Even if you don't use hammers, you can still benefit from having global resource limits.
+# (When in hammer-less context, this global variable is checked by LanguageModelAPI. Only the financial limits are enforced.)
+
+_GLOBAL_RESOURCE_LIMITS: ResourceLimits | None = None
+
+
+def ensure_global_resource_limits(
+ *,
+ max_dollars: float | None = None,
+ max_seconds: float | None = None,
+ warn_fraction: float | None = None,
+ dollars_per_hour: float | None = None,
+ reset_if_already_set: bool = False,
+) -> None:
+ global _GLOBAL_RESOURCE_LIMITS
+ if _GLOBAL_RESOURCE_LIMITS is None or reset_if_already_set:
+ _GLOBAL_RESOURCE_LIMITS = ResourceLimits.build(
+ max_dollars=max_dollars,
+ max_seconds=max_seconds,
+ warn_fraction=warn_fraction,
+ dollars_per_hour=dollars_per_hour,
+ )
+ _GLOBAL_RESOURCE_LIMITS.save_spend_callback = _dummy_save_spend_callback
+
+
+def get_global_resource_limits() -> ResourceLimits | None:
+ return _GLOBAL_RESOURCE_LIMITS
+
+
+async def _dummy_save_spend_callback(dollars: float) -> None:
+ pass
+
+
+async def get_global_resource_limits_summary() -> str:
+ if _GLOBAL_RESOURCE_LIMITS is None:
+ return "No global resource limits"
+ output = [
+ "Global resource limits summary:",
+ ]
+ max_dollars = _GLOBAL_RESOURCE_LIMITS.hard_cap_dollars
+ amount_spent = await _GLOBAL_RESOURCE_LIMITS.get_dollars_authorized_and_spent()
+ amount_remaining = max_dollars - amount_spent
+ output.append(f"- Max dollars: ${max_dollars:.4f}")
+ output.append(f"- Amount spent: ${amount_spent:.4f}")
+ output.append(f"- Amount remaining: ${amount_remaining:.4f}")
+ return "\n".join(output)
diff --git a/vet/imbue_core/async_monkey_patches.py b/vet/imbue_core/async_monkey_patches.py
@@ -0,0 +1,343 @@
+import asyncio
+import sys
+import traceback
+from asyncio import Future
+from types import TracebackType
+from typing import Any
+from typing import Sequence
+
+from loguru import logger
+
+from vet.imbue_core.errors import ExpectedError
+
+_IS_SHUTTING_DOWN = False
+
+
+def notify_task_groups_of_shutdown() -> None:
+ global _IS_SHUTTING_DOWN
+ _IS_SHUTTING_DOWN = True
+
+
+class PropagatingTaskGroup(asyncio.TaskGroup):
+ """Improves over TaskGroup by ensuring that cancelation messages are actually propagated"""
+
+ def __init__(self) -> None:
+ # deferring the import in case this doesn't get used
+ pass
+
+ python_version: sys._version_info = sys.version_info
+ if python_version[:2] != (3, 11) and python_version[:2] != (3, 12):
+ raise RuntimeError(
+ f"Python version 3.11 or 3.12 is required. You are using {python_version.major}.{python_version.minor}"
+ )
+ super().__init__()
+ self._entered = False
+ self._exiting = False
+ self._aborting = False
+ self._loop = None
+ self._parent_task: asyncio.Task | None = None
+ self._parent_cancel_requested = False
+ self._tasks: set[asyncio.Task] = set()
+ self._errors: list[BaseException] = []
+ self._base_error: BaseException | None = None
+ self._on_completed_fut: Future[Any] | None = None
+ self._original_message: str | None = None
+
+ async def __aexit__(
+ self,
+ et: type[BaseException] | None,
+ exc: BaseException | None,
+ tb: TracebackType | None,
+ ) -> None:
+ try:
+ return await self._aexit(et, exc)
+ finally:
+ # Exceptions are heavy objects that can have object
+ # cycles (bad for GC); let's not keep a reference to
+ # a bunch of them. It would be nicer to use a try/finally
+ # in __aexit__ directly but that introduced some diff noise
+ self._parent_task = None
+ self._errors = [] # Clear the list instead of setting to None
+ self._base_error = None
+ exc = None
+
+ async def _aexit(self, et: type[BaseException] | None, exc: BaseException | None) -> None:
+ self._exiting = True
+
+ if exc is not None and self._is_base_error(exc) and self._base_error is None: # type: ignore
+ self._base_error = exc
+
+ propagate_cancellation_error = exc if et is asyncio.exceptions.CancelledError else None
+ if self._parent_cancel_requested:
+ # If this flag is set we *must* call uncancel().
+ assert self._parent_task
+ if self._parent_task.uncancel() == 0:
+ # If there are no pending cancellations left,
+ # don't propagate CancelledError.
+ propagate_cancellation_error = None
+
+ if et is not None:
+ if not self._aborting:
+ # Our parent task is being cancelled:
+ #
+ # async with TaskGroup() as g:
+ # g.create_task(...)
+ # await ... # <- CancelledError
+ #
+ # or there's an exception in "async with":
+ #
+ # async with TaskGroup() as g:
+ # g.create_task(...)
+ # 1 / 0
+ assert exc is not None
+ self._abort_and_propagate(exc)
+
+ # We use while-loop here because "self._on_completed_fut"
+ # can be cancelled multiple times if our parent task
+ # is being cancelled repeatedly (or even once, when
+ # our own cancellation is already in progress)
+ while self._tasks:
+ if self._on_completed_fut is None:
+ assert self._loop
+ self._on_completed_fut = self._loop.create_future()
+
+ try:
+ await self._on_completed_fut
+ except asyncio.exceptions.CancelledError as ex:
+ if not self._aborting:
+ # Our parent task is being cancelled:
+ #
+ # async def wrapper():
+ # async with TaskGroup() as g:
+ # g.create_task(foo)
+ #
+ # "wrapper" is being cancelled while "foo" is
+ # still running.
+ propagate_cancellation_error = ex
+ self._abort_and_propagate(ex)
+
+ self._on_completed_fut = None
+
+ assert not self._tasks
+
+ if self._base_error is not None:
+ try:
+ raise self._base_error
+ finally:
+ exc = None
+
+ # Propagate CancelledError if there is one, except if there
+ # are other errors -- those have priority.
+ try:
+ if propagate_cancellation_error and not self._errors:
+ try:
+ raise propagate_cancellation_error
+ finally:
+ exc = None
+ finally:
+ propagate_cancellation_error = None
+
+ if et is not None and et is not asyncio.exceptions.CancelledError:
+ assert exc is not None
+ self._errors.append(exc)
+
+ if self._errors:
+ try:
+ raise BaseExceptionGroup(
+ "unhandled errors in a TaskGroup: see earlier in logs for causal error!",
+ self._errors,
+ ) from None
+ finally:
+ exc = None
+
+ def _abort(self) -> None:
+ raise Exception("Please call _abort_and_propagate instead")
+
+ def _abort_and_propagate(self, exc: BaseException) -> None:
+ global _IS_SHUTTING_DOWN
+ self._aborting = True
+
+ if self._original_message is None:
+ if isinstance(exc, asyncio.exceptions.CancelledError) and len(exc.args) > 0:
+ self._original_message = "TaskGroup canceled because:\n" + exc.args[0]
+ else:
+ if not isinstance(exc, asyncio.exceptions.CancelledError):
+ if not _IS_SHUTTING_DOWN and not isinstance(exc, ExpectedError):
+ log_exception(
+ exc,
+ "Emergency print of error that caused task group to die:",
+ )
+ self._original_message = f"TaskGroup died because: {type(exc).__name__}: {exc}\n" + "".join(
+ traceback.extract_tb(exc.__traceback__).format()
+ )
+
+ for t in self._tasks:
+ if not t.done():
+ t.cancel(self._original_message)
+
+ def _on_task_done(self, task: asyncio.Task) -> None:
+ self._tasks.discard(task)
+
+ on_completed_fut = self._on_completed_fut
+ if on_completed_fut is not None and not self._tasks:
+ if not on_completed_fut.done():
+ on_completed_fut.set_result(True)
+
+ if task.cancelled():
+ return
+
+ exc = task.exception()
+ if exc is None:
+ return
+
+ self._errors.append(exc)
+ if self._is_base_error(exc) and self._base_error is None: # type: ignore
+ self._base_error = exc
+
+ parent_task = self._parent_task
+ assert parent_task
+ if parent_task.done():
+ # Not sure if this case is possible, but we want to handle
+ # it anyways.
+ assert self._loop
+ self._loop.call_exception_handler(
+ {
+ "message": f"Task {task!r} has errored out but its parent task {parent_task} is already completed",
+ "exception": exc,
+ "task": task,
+ }
+ )
+ return
+
+ if not self._aborting and not self._parent_cancel_requested:
+ # If parent task *is not* being cancelled, it means that we want
+ # to manually cancel it to abort whatever is being run right now
+ # in the TaskGroup. But we want to mark parent task as
+ # "not cancelled" later in __aexit__. Example situation that
+ # we need to handle:
+ #
+ # async def foo():
+ # try:
+ # async with TaskGroup() as g:
+ # g.create_task(crash_soon())
+ # await something # <- this needs to be canceled
+ # # by the TaskGroup, e.g.
+ # # foo() needs to be cancelled
+ # except Exception:
+ # # Ignore any exceptions raised in the TaskGroup
+ # pass
+ # await something_else # this line has to be called
+ # # after TaskGroup is finished.
+ self._abort_and_propagate(exc)
+ self._parent_cancel_requested = True
+ parent_task.cancel(self._original_message)
+
+
+def safe_cancel(task: asyncio.Task, msg: str | None = None) -> None:
+ """
+ NOTE: this is probably not what you want! See safe_cancel_and_wait_for_cleanup below for the more common use case.
+
+ Cancels a task in a way that preserves information about who canceled it.
+
+ Without using this, it is super obnoxious to figure out why your function is being canceled --
+ you just get a CancelledError with no traceback.
+
+ We try to ensure that *all* of our tasks are canceled in this way, which makes debugging much easier.
+
+ Even safe_cancel_and_wait_for_cleanup will cancel in this way. The only difference is that that function
+ also waits for the task to actually be canceled. Otherwise, cancellation just enqueues a cancellation.
+
+ Note also that cancellation is never guaranteed -- all it does is raise a CancelledError in the task.
+ This is why it is so important to never swallow those errors!
+ """
+ task.is_being_canceled_by_us = True # type: ignore
+ message = f"Task canceled by: \n {''.join(traceback.format_stack()[:-1])}"
+ if msg:
+ message += f"\nOriginal message: {msg}"
+
+ task.cancel(message)
+
+
+async def safe_cancel_and_wait_for_cleanup(
+ task: asyncio.Task,
+ msg: str | None = None,
+ exception_types_to_ignore: Sequence[type[BaseException]] = (),
+) -> None:
+ """
+ Convenience function for calling safe_cancel_multiple_and_wait_for_cleanup with a single task.
+
+ See safe_cancel_multiple_and_wait_for_cleanup for docs.
+ """
+ await safe_cancel_multiple_and_wait_for_cleanup([task], msg, exception_types_to_ignore)
+
+
+async def safe_cancel_multiple_and_wait_for_cleanup(
+ tasks: Sequence[asyncio.Task],
+ msg: str | None = None,
+ exception_types_to_ignore: Sequence[type[BaseException]] = (),
+) -> None:
+ """
+ Calls safe_cancel (see docs above) on each task in tasks, then waits for them to be done.
+
+ Note that you can pass in a list of exception types to ignore.
+ This is important for suppressing exceptions from third party libraries.
+ You should probably make a constant in your project that lists these exceptions.
+
+ We cannot simply suppress all BaseExceptions here because you really don't want to do that for things like signals and OutOfMemoryError
+ """
+ for task in tasks:
+ safe_cancel(task, msg)
+ # if you really want something to be canceled, you need to wait for it to be done
+ # https://docs.python.org/3/library/asyncio-task.html#asyncio.Task.cancel
+ results = await asyncio.gather(*tasks, return_exceptions=True)
+ exceptions = []
+ for result in results:
+ if isinstance(result, BaseException):
+ if isinstance(result, asyncio.CancelledError):
+ pass
+ elif isinstance(result, ExceptionGroup):
+ exceptions.extend(_filter_exception_group(result))
+ else:
+ exceptions.append(result) # type: ignore
+ # cannot do this because the task may have just finished
+ # assert (
+ # False
+ # ), f"While cancelling async task and waiting for cleanup, expected None or CancelledError, got `{type(x)}: {x}`"
+
+ filtered_exceptions = []
+ for exception in exceptions:
+ if not any(isinstance(exception, exception_type) for exception_type in exception_types_to_ignore):
+ filtered_exceptions.append(exception)
+
+ if len(filtered_exceptions) == 1:
+ raise filtered_exceptions[0]
+ elif len(filtered_exceptions) > 1:
+ raise ExceptionGroup("Multiple exceptions in task group while canceling", filtered_exceptions)
+
+
+def _filter_exception_group(exc_group: ExceptionGroup) -> list[Exception]:
+ """Recursively extract exceptions from ExceptionGroups (ignoring canceled errors)."""
+ result = []
+ for exc in exc_group.exceptions:
+ if isinstance(exc, asyncio.CancelledError):
+ continue
+ elif isinstance(exc, ExceptionGroup):
+ result.extend(_filter_exception_group(exc))
+ else:
+ result.append(exc)
+ return result
+
+
+def log_exception(
+ exc: BaseException,
+ message: str,
+ *args: Any,
+ **kwargs: Any,
+) -> None:
+ """Log an exception with its traceback to stderr via loguru."""
+ logger.opt(exception=exc).error(message, *args, **kwargs)
+
+
+def apply() -> None:
+ asyncio.TaskGroup = PropagatingTaskGroup # type: ignore
+ asyncio.taskgroups.TaskGroup = PropagatingTaskGroup # type: ignore
diff --git a/imbue_core/imbue_core/async_monkey_patches_test.py b/vet/imbue_core/async_monkey_patches_test.py
diff --git a/imbue_core/imbue_core/async_utils.py b/vet/imbue_core/async_utils.py
diff --git a/vet/imbue_core/caching.py b/vet/imbue_core/caching.py
@@ -0,0 +1,183 @@
+from __future__ import annotations
+
+import asyncio
+from functools import lru_cache
+from pathlib import Path
+from types import TracebackType
+from typing import Generic
+from typing import Self
+from typing import Sequence
+from typing import TypeVar
+
+from diskcache import Cache
+from diskcache import JSONDisk
+
+from vet.imbue_core.cattrs_serialization import deserialize_from_json
+from vet.imbue_core.cattrs_serialization import serialize_to_json
+from vet.imbue_core.frozen_utils import FrozenDict
+from vet.imbue_core.frozen_utils import FrozenMapping
+
+ValueType = TypeVar("ValueType", covariant=True)
+
+
+class AsyncCacheInterface(Generic[ValueType]):
+ async def __aenter__(self) -> Self:
+ raise NotImplementedError()
+
+ async def __aexit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ raise NotImplementedError()
+
+ async def set(
+ self,
+ key: str,
+ # pyre-fixme[46]: ValueType is covariant
+ value: ValueType,
+ expire: int | None = None,
+ read: bool = False,
+ tag: str | None = None,
+ retry: bool = False,
+ ) -> bool:
+ raise NotImplementedError()
+
+ async def get(
+ self,
+ key: str,
+ default: ValueType | None = None,
+ read: bool = False,
+ expire_time: bool = False,
+ tag: bool = False,
+ retry: bool = False,
+ ) -> ValueType | None:
+ raise NotImplementedError()
+
+ async def get_all(
+ self,
+ keys: Sequence[str],
+ default: ValueType | None = None,
+ read: bool = False,
+ expire_time: bool = False,
+ tag: bool = False,
+ retry: bool = False,
+ ) -> FrozenMapping[str, ValueType | None]:
+ raise NotImplementedError()
+
+ async def get_all_keys(self, reverse: bool = False) -> tuple[str, ...]:
+ raise NotImplementedError()
+
+
+class AsyncCache(AsyncCacheInterface[ValueType], Generic[ValueType]):
+ def __init__(self, path: Path, value_cls: type[ValueType]) -> None:
+ self.path = path
+ self.value_cls = value_cls
+ self.cache: Cache | None = None
+
+ async def _build_cache(self) -> Cache:
+ loop = asyncio.get_running_loop()
+ # pyre-ignore[6]: pyre doesn't like the lru cache here
+ return await loop.run_in_executor(None, get_cache, self.path)
+
+ async def __aenter__(self) -> Self:
+ loop = asyncio.get_running_loop()
+ cache = await self._build_cache()
+ self.cache = cache
+ await loop.run_in_executor(None, cache.__enter__)
+ return self
+
+ async def __aexit__(
+ self,
+ exc_type: type[BaseException] | None,
+ exc_val: BaseException | None,
+ exc_tb: TracebackType | None,
+ ) -> None:
+ loop = asyncio.get_running_loop()
+ cache = self.cache
+ assert cache is not None
+ result = await loop.run_in_executor(
+ None, cache.__exit__, exc_type, exc_val, exc_tb
+ )
+ self.cache = None
+ return result
+
+ async def set(
+ self,
+ key: str,
+ # pyre-fixme[46]: ValueType is covariant
+ value: ValueType,
+ expire: int | None = None,
+ read: bool = False,
+ tag: str | None = None,
+ retry: bool = False,
+ ) -> bool:
+ cache = self.cache
+ assert cache is not None
+ loop = asyncio.get_running_loop()
+ assert isinstance(value, self.value_cls), (
+ f"Expected {self.value_cls}, got {type(value)}"
+ )
+ serialized_value = serialize_to_json(value)
+ return await loop.run_in_executor(
+ None, cache.set, key, serialized_value, expire, read, tag, retry
+ )
+
+ async def get(
+ self,
+ key: str,
+ default: ValueType | None = None,
+ read: bool = False,
+ expire_time: bool = False,
+ tag: bool = False,
+ retry: bool = False,
+ ) -> ValueType | None:
+ cache = self.cache
+ assert cache is not None
+ loop = asyncio.get_running_loop()
+ value = await loop.run_in_executor(
+ None, cache.get, key, None, read, expire_time, tag, retry
+ )
+ if value is None:
+ return default
+ deserialized_value = deserialize_from_json(value)
+ assert isinstance(deserialized_value, self.value_cls), (
+ f"Expected {self.value_cls}, got {type(deserialized_value)}"
+ )
+ return deserialized_value
+
+ # TODO: this is not smart implementation, but at least it will be possible to optimize later without refactoring
+ async def get_all(
+ self,
+ keys: Sequence[str],
+ default: ValueType | None = None,
+ read: bool = False,
+ expire_time: bool = False,
+ tag: bool = False,
+ retry: bool = False,
+ ) -> FrozenMapping[str, ValueType | None]:
+ tasks = {}
+ for key in keys:
+ tasks[key] = self.get(key, default, read, expire_time, tag, retry)
+ results = await asyncio.gather(*tasks.values())
+ return FrozenDict(zip(tasks.keys(), results))
+
+ # TODO: might be nice to get iterkeys back someday, but whatever for now, too annoying to get the sync/async right
+ async def get_all_keys(self, reverse: bool = False) -> tuple[str, ...]:
+ cache = self.cache
+ assert cache is not None
+ loop = asyncio.get_running_loop()
+ return tuple(await loop.run_in_executor(None, cache.iterkeys, reverse))
+
+
+@lru_cache
+def get_cache(data_path: Path) -> Cache:
+ # not sure if the size limit applies when eviction is none, but ~64GB should be enough for now
+ return Cache(
+ str(data_path),
+ disk=JSONDisk,
+ disk_compress_level=0,
+ eviction_policy="none",
+ size_limit=2**36,
+ )
diff --git a/vet/imbue_core/cattrs_serialization.py b/vet/imbue_core/cattrs_serialization.py
@@ -0,0 +1,991 @@
+import abc
+import asyncio
+import base64
+import builtins
+import datetime
+import functools
+import importlib
+import inspect
+import json
+from decimal import Decimal
+from enum import Enum
+from functools import cached_property
+from functools import lru_cache
+from functools import partial
+from pathlib import Path
+from pathlib import PosixPath
+from types import NoneType
+from types import UnionType
+from typing import Any
+from typing import Callable
+from typing import ForwardRef
+from typing import Hashable
+from typing import Mapping
+from typing import TypeVar
+from typing import Union
+from typing import cast
+from typing import get_origin
+from uuid import UUID
+
+import anyio
+import attr
+from cachetools import LRUCache
+from cattrs import Converter
+from cattrs._compat import is_generic
+from cattrs.gen import make_dict_unstructure_fn
+from cattrs.gen import override
+from httpx import URL
+from humps import camelize # pyre-ignore[21]: pyre doesn't understand this import
+from pydantic import BaseModel
+from pydantic_core import PydanticUndefined
+
+from vet.imbue_core.errors import ImbueError
+from vet.imbue_core.fixed_traceback import FixedTraceback
+from vet.imbue_core.frozen_utils import FrozenDict
+from vet.imbue_core.frozen_utils import FrozenMapping
+from vet.imbue_core.serialization import SerializedException
+from vet.imbue_core.serialization_types import Serializable
+
+T = TypeVar("T")
+TYPE_KEY = "__type"
+EXCEPTION_KEY = "__exception"
+
+# LABELS for marking attributes with special handling
+DONT_SERIALIZE_METADATA_KEY = "_imbue_dont_serialize"
+DONT_SERIALIZE = {DONT_SERIALIZE_METADATA_KEY: True}
+SERIALIZE_WITH_DEFAULT_KEY = "_imbue_serialize_with_default"
+SERIALIZE_WITH_DEFAULT = {SERIALIZE_WITH_DEFAULT_KEY: True}
+
+SERIALIZABLE_PROPERTY_KEY = "_imbue_is_serializable_property"
+CACHED_SERIALIZABLE_PROPERTY_KEY = "_imbue_is_cached_serializable_property"
+
+
+##########################################################################################
+# UTILITY FUNCTIONS
+##########################################################################################
+
+
+def _safe_issubclass(t1: type, t2: type) -> bool:
+ return inspect.isclass(t1) and issubclass(t1, t2)
+
+
+def _is_frozen_mapping_type(t: type) -> bool:
+ return _safe_issubclass(get_origin(t) or t, FrozenMapping)
+
+
+def _is_mapping_type(t: type) -> bool:
+ return _safe_issubclass(get_origin(t) or t, Mapping)
+
+
+_ALLOWED_SPECIAL_MAPPING_TYPES = (LRUCache,)
+
+
+def _is_special_mapping_type(t: type) -> bool:
+ return t in _ALLOWED_SPECIAL_MAPPING_TYPES
+
+
+def _is_str_type_special_mapping_type(t: str) -> bool:
+ return t in [_type_to_string(t, fully_qualified=True) for t in _ALLOWED_SPECIAL_MAPPING_TYPES]
+
+
+def _is_obj_supported_primitive(obj: Any) -> bool:
+ return type(obj) in {bool, int, float, str, NoneType}
+
+
+def _type_to_string(t: type, fully_qualified: bool) -> str:
+ name = t.__name__
+ if fully_qualified:
+ return f"{t.__module__}.{name}"
+ else:
+ return name
+
+
+def _type_from_string(type_str: str) -> Any:
+ if "[" in type_str:
+ class_details, _ = type_str.split("[", 1)
+ else:
+ class_details = type_str
+ if "." in class_details:
+ module_path, class_name = class_details.rsplit(".", 1)
+ module = importlib.import_module(module_path)
+ else:
+ class_name = class_details
+ module = builtins
+ result = getattr(module, class_name)
+ return result
+
+
+def get_serializable_properties(obj: Any) -> dict[str, Any]:
+ members = inspect.getmembers(type(obj))
+ marked_members = {}
+ for name, member in members:
+ if is_serializable_property(member):
+ marked_members[name] = getattr(obj, name)
+ return marked_members
+
+
+def is_serializable_property(func: Callable) -> bool:
+ return getattr(func, CACHED_SERIALIZABLE_PROPERTY_KEY, False) or (
+ isinstance(func, property) and getattr(func.fget, SERIALIZABLE_PROPERTY_KEY, False)
+ )
+
+
+def cached_serializable_property(func: Callable[..., T]) -> cached_property[T]:
+ property_to_return = cached_property(func)
+ setattr(property_to_return, CACHED_SERIALIZABLE_PROPERTY_KEY, True)
+ return property_to_return
+
+
+def serializable_property(func: Callable[..., T]) -> property:
+ property_to_return = func
+ # NOTE: this will be stored in the fget attribute of the property, which is also the function
+ # we are decorating, so we must check in `func.fget` to see if the property is serializable.
+ # We need to do it this way because we cannot set the attribute on the property object/wrapper
+ # itself, because of the way the inbuilt `property` decorator works.
+ setattr(property_to_return, SERIALIZABLE_PROPERTY_KEY, True)
+ return property(property_to_return)
+
+
+def get_dont_serialize_member_names_of_type(obj_type: type) -> list[str]:
+ if not attr.has(obj_type):
+ return []
+ return [field.name for field in attr.fields(obj_type) if field.metadata.get(DONT_SERIALIZE_METADATA_KEY, False)]
+
+
+def get_serialize_with_default_member_names_of_type(
+ obj_type: type,
+) -> Mapping[str, Any]:
+ if _safe_issubclass(obj_type, BaseModel):
+ model_fields = getattr(obj_type, "model_fields", {})
+ return {
+ name: None if field.default == PydanticUndefined else field.default for name, field in model_fields.items()
+ }
+ if not attr.has(obj_type):
+ return {}
+ return {
+ field.name: None if field.default == attr.NOTHING else field.default
+ for field in attr.fields(obj_type)
+ if field.metadata.get(SERIALIZE_WITH_DEFAULT_KEY, False)
+ }
+
+
+def get_dont_serialize_member_names(obj: Any) -> list[str]:
+ if not attr.has(obj):
+ return []
+ members = inspect.getmembers(obj)
+ marked_members = []
+ for name, _ in members:
+ if is_dont_serialize_member(obj, name):
+ marked_members.append(name)
+ return marked_members
+
+
+def is_dont_serialize_member(obj: Any, member_name: str) -> bool:
+ if not attr.has(obj):
+ return False
+ for field in attr.fields(obj.__class__): # type: ignore
+ if field.name == member_name:
+ return bool(field.metadata.get(DONT_SERIALIZE_METADATA_KEY, False))
+ return False
+
+
+class SerializationError(ImbueError):
+ """Raised when we encounter problems related to Serialization or Deserialization."""
+
+
+def _to_json_dumpable_object_without_type_keys(data: Any) -> Any:
+ if isinstance(data, dict):
+ if data.get(TYPE_KEY, "") in {
+ _type_to_string(PosixPath, fully_qualified=True),
+ _type_to_string(Path, fully_qualified=True),
+ _type_to_string(UUID, fully_qualified=True),
+ }:
+ return data["value"]
+ else:
+ return {
+ key: _to_json_dumpable_object_without_type_keys(value) for key, value in data.items() if key != TYPE_KEY
+ }
+ elif isinstance(data, list):
+ return [_to_json_dumpable_object_without_type_keys(item) for item in data]
+ elif _is_obj_supported_primitive(data):
+ return data
+ else:
+ return str(data)
+
+
+def _camelize_keys_which_represent_python_names(data: Any) -> Any:
+ """Converts JSON-style objects to use camel case keys.
+
+ Takes a JSON-style object produced by CONVERTER.structure and returns the same object with certain
+ keys converted to camel case. Camel cases keys which are derived from names of Python attributes and properties.
+ Does not camel-case keys which were keys of dictionaries before serialization.
+
+ See cattrs_serialization_test.test_camel_casing for an example.
+ """
+ if isinstance(data, dict):
+ if TYPE_KEY not in data or issubclass(_type_from_string(data[TYPE_KEY]), Mapping):
+ return {key: _camelize_keys_which_represent_python_names(value) for key, value in data.items()}
+ else:
+ # pyre-ignore[16]: pyre doesn't understand the import of camelize
+ return {camelize(key): _camelize_keys_which_represent_python_names(value) for key, value in data.items()}
+ elif isinstance(data, list):
+ return [_camelize_keys_which_represent_python_names(item) for item in data]
+ else:
+ return data
+
+
+##########################################################################################
+# CLASS-SPECIFIC HOOKS
+##########################################################################################
+
+
+class _ShouldDeserialize:
+ pass
+
+
+# FIXME: Types such as LRUCache will always serialize without errors since they inherit from Mapping but will not deserialize correctly.
+# We should either document this behavior or change it so that the serialization fails if the type is not supported.
+def _serialize_mapping_to_json_dict(data: Mapping, converter: Converter) -> Any:
+ assert _is_mapping_type(type(data)), f"Attempted to serialize object of type {type(data)} as a mapping."
+ return {str(converter.unstructure(k)): converter.unstructure(v) for k, v in data.items()}
+
+
+def _serialize_mapping(data: Mapping, converter: Converter) -> Any:
+ assert _is_mapping_type(type(data)), f"Attempted to serialize object of type {type(data)} as a mapping."
+ entries = [(converter.unstructure(k), converter.unstructure(v)) for k, v in data.items()]
+ return {
+ TYPE_KEY: _type_to_string(type(data), fully_qualified=True),
+ "__entries": entries,
+ }
+
+
+def _deserialize_special_mapping_types(data: dict, type_key: str) -> Mapping:
+ if type_key == _type_to_string(LRUCache, fully_qualified=True):
+ # FIXME: We're not serializing the object correctly and so the deserialization is hacky
+ obj: LRUCache = LRUCache(maxsize=10000)
+ return obj
+ else:
+ raise ValueError(f"Unsupported type {type_key}")
+
+
+def _deserialize_mapping(data: dict, mapping_type: type, converter: Converter) -> Mapping:
+ if TYPE_KEY in data and _is_str_type_special_mapping_type(data[TYPE_KEY]):
+ return _deserialize_special_mapping_types(data, data[TYPE_KEY])
+
+ out = {}
+ if "__entries" in data:
+ entries = data["__entries"]
+ else:
+ # We keep this branch for backwards compatibility with mappings serialized as dictionaries.
+ # We do not support Yasoo's DictWithSerializedKeys -- those will need to be migrated to the new format.
+ if TYPE_KEY in data:
+ del data[TYPE_KEY]
+ entries = data.items()
+
+ for k, v in entries:
+ out[converter.structure(k, _ShouldDeserialize)] = converter.structure(v, _ShouldDeserialize)
+
+ if _is_frozen_mapping_type(mapping_type):
+ return FrozenDict(out)
+ return out
+
+
+def _serialize_frozen_set(data: frozenset, converter: Converter) -> dict:
+ assert type(data) is frozenset, f"Attempted to serialize object of type {type(data)} as a frozenset."
+ value = converter.unstructure(data, unstructure_as=list)
+ return {"value": value, TYPE_KEY: _type_to_string(type(data), fully_qualified=True)}
+
+
+def _deserialize_frozen_set(data: dict, _: type, converter: Converter) -> frozenset:
+ return frozenset(converter.structure(data["value"], list))
+
+
+def _serialize_uuid(data: UUID) -> dict:
+ if type(data) is UUID:
+ return {
+ "value": data.hex,
+ TYPE_KEY: _type_to_string(type(data), fully_qualified=True),
+ }
+ elif type(data) is str:
+ return {"value": data, TYPE_KEY: _type_to_string(UUID, fully_qualified=True)}
+ else:
+ raise TypeError("Tried to serialize " + str(data) + ", which is neither a string nor a UUID, as a UUID.")
+
+
+def _deserialize_uuid(data: dict[str, str] | str, _: type) -> UUID:
+ if isinstance(data, dict):
+ return UUID(data["value"])
+ elif isinstance(data, str):
+ return UUID(data)
+ else:
+ raise TypeError("Tried to deserialize something which is neither a string nor a dictionary, as a UUID.")
+
+
+def _serialize_tuple(data: tuple, converter: Converter) -> dict:
+ assert type(data) is tuple, f"Attempted to serialize object of type {type(data)} as a tuple."
+ return {
+ "value": [converter.unstructure(x) for x in data],
+ TYPE_KEY: _type_to_string(type(data), fully_qualified=True),
+ }
+
+
+def _deserialize_tuple(data: dict, _: type, converter: Converter) -> tuple:
+ return tuple(converter.structure(x, _ShouldDeserialize) for x in data["value"])
+
+
+def _serialize_url(data: URL) -> dict:
+ assert type(data) is URL, f"Tried to serialize {data} which is not a URL."
+ return {
+ "value": str(data),
+ TYPE_KEY: _type_to_string(type(data), fully_qualified=True),
+ }
+
+
+def _deserialize_url(data: dict, _: type) -> URL:
+ return URL(data["value"])
+
+
+def _serialize_decimal(data: Decimal) -> dict:
+ assert type(data) is Decimal, f"Attempted to serialize object of type {type(data)} as a Decimal."
+ return {
+ "value": str(data),
+ TYPE_KEY: _type_to_string(type(data), fully_qualified=True),
+ }
+
+
+def _deserialize_decimal(data: dict, _: type) -> Decimal:
+ return Decimal(data["value"])
+
+
+def _serialize_traceback(data: FixedTraceback) -> dict:
+ assert _safe_issubclass(
+ type(data), FixedTraceback
+ ), f"Attempted to serialize object of type {type(data)} as a traceback."
+ return {
+ "value": data.to_dict(),
+ TYPE_KEY: _type_to_string(type(data), fully_qualified=True),
+ }
+
+
+def _deserialize_traceback(data: dict, _: type) -> FixedTraceback:
+ return FixedTraceback.from_dict(data["value"])
+
+
+def _serialize_path(data: Path) -> dict:
+ assert _safe_issubclass(type(data), Path), f"Attempted to serialize an object of type {type(data)} as a Path."
+ return {
+ "value": str(data),
+ TYPE_KEY: _type_to_string(type(data), fully_qualified=True),
+ }
+
+
+def _deserialize_path(data: Any, _: type) -> Path:
+ if type(data) is dict:
+ return Path(data["value"])
+ return Path(data)
+
+
+def _serialize_anyio_path(data: anyio.Path) -> dict:
+ assert _safe_issubclass(type(data), anyio.Path), f"Attempted to serialize an object of type {type(data)} as a Path."
+ return {
+ "value": str(data),
+ TYPE_KEY: _type_to_string(type(data), fully_qualified=True),
+ }
+
+
+def _deserialize_anyio_path(data: Any, _: type) -> anyio.Path:
+ if type(data) is dict:
+ return anyio.Path(data["value"])
+ return anyio.Path(data)
+
+
+def _serialize_datetime(data: datetime.datetime) -> dict:
+ assert _safe_issubclass(
+ type(data), datetime.datetime
+ ), f"Attempted to serialize object of type {type(data)} as a datetime."
+ return {
+ TYPE_KEY: _type_to_string(type(data), fully_qualified=True),
+ "time": data.astimezone(datetime.timezone.utc).timestamp(),
+ "tzaware": data.tzinfo is not None,
+ }
+
+
+def _deserialize_datetime(data: dict, _: type) -> datetime.datetime:
+ return datetime.datetime.fromtimestamp(data["time"], datetime.timezone.utc if data.get("tzaware", None) else None)
+
+
+def _serialize_bytes(data: bytes) -> dict:
+ assert type(data) is bytes, f"Attempted to serialize object of type {type(data)} as bytes."
+ return {
+ TYPE_KEY: _type_to_string(type(data), fully_qualified=True),
+ # use ascii since base64 guarantees ascii characters only
+ "value": base64.b64encode(data).decode("ascii"),
+ }
+
+
+def _deserialize_bytes(data: dict, _: type) -> bytes:
+ return base64.b64decode(data["value"])
+
+
+def _is_forward_ref(t: type) -> bool:
+ return isinstance(t, ForwardRef)
+
+
+def _serialize_forward_ref(data: Any, converter: Converter) -> Any:
+ return converter.unstructure(data, unstructure_as=type(data))
+
+
+def _deserialize_forward_ref(data: Any, _: type, converter: Converter) -> Any:
+ # TODO: think of a way to evaluate the ForwardRef _, to improve type safety.
+ # Once we do that, we can swap out the evaluated type for ShouldDeserialize
+ # and enforce that we're getting an object of the correct type.
+ return _deserialize_serialized_object(data, _ShouldDeserialize, converter)
+
+
+def _is_union_type(t: type) -> bool:
+ origin = get_origin(t)
+ return origin is Union or origin is UnionType
+
+
+def _deserialize_union_type(data: Any, type_of_data: type, converter: Converter) -> Any:
+ return converter.structure(data, _ShouldDeserialize)
+
+
+def _serialize_enum(data: Enum, converter: Converter) -> Any:
+ assert inspect.isclass(type(data)) and issubclass(
+ type(data), Enum
+ ), f"Attempted to serialize object of type {type(data)} as an Enum."
+ return converter._unstructure_enum(data)
+
+
+def _deserialize_enum(data: dict[str, str] | str, t: type) -> Any:
+ # We include this complicated logic to preserve backwards compatibility with old JSON that was
+ # serialized by Yasoo. Yasoo serialized enums by converting them into the form
+ # {"__type": "...", "value": "..."}. Strangely, Yasoo converted this dictionary into a string
+ # whenever an enum value occurred as a dictionary key, but did not convert it into a string
+ # when it occurred anywhere else. Hence we need to handle enums that are represented by
+ # dictionaries, stringified dictionaries, and strings.
+
+ assert _safe_issubclass(t, Enum)
+
+ if isinstance(data, str):
+ try:
+ # This is the case where data is an enum value, serialized by Cattrs.
+ return t(data)
+ except ValueError:
+ # This is the case where data is a stringified dictionary, serialized by Yasoo.
+ data_as_dict = json.loads(data)
+ return t[data_as_dict["value"]] # type: ignore
+ else:
+ # This is the case where data is a dictionary, serialized by Yasoo.
+ return t[data["value"]] # type: ignore
+
+
+##########################################################################################
+# TYPE KEY LOGIC
+##########################################################################################
+
+
+class _AvoidTypeKeyLogic:
+ pass
+
+
+@lru_cache
+def flag_to_ignore_type_key_hooks(t: type) -> type:
+ class GivenTypeFlaggedToAvoidTypeKeyLogic(t, _AvoidTypeKeyLogic):
+ pass
+
+ GivenTypeFlaggedToAvoidTypeKeyLogic.__name__ = t.__name__
+ GivenTypeFlaggedToAvoidTypeKeyLogic.__qualname__ = t.__qualname__
+
+ # pyre-fixme[16]: pyre doesn't understand dynamically created classes
+ return GivenTypeFlaggedToAvoidTypeKeyLogic
+
+
+def get_pydantic_model_attributes(model: BaseModel) -> dict[str, Any]:
+ # This is a hack to dump only the top level but also avoid dumping any properties
+ attributes = getattr(type(model), "model_fields", {})
+ return {a: getattr(model, a) for a in attributes}
+
+
+# These two factory functions produce the functions for serializing attr classes.
+# Only one of them should be registered at a time, depending on whether we are including
+# do-not-serialize fields in the serialization.
+def _serialize_attr_class_factory(cls: type, converter: Converter) -> Callable[[Any], Any]:
+ return make_dict_unstructure_fn(cls, converter)
+
+
+def _serialize_attr_class_without_dont_serialize_fields(
+ cls: type, converter: Converter, is_camel_case: bool
+) -> Callable[[Any], Any]:
+ members_to_omit = get_dont_serialize_member_names_of_type(cls)
+ omit_kwargs = {name: override(omit=True) for name in members_to_omit}
+ return make_dict_unstructure_fn(cls, converter, **omit_kwargs) # type: ignore
+
+
+def _serialize_with_type_key(data: Any, converter: Converter, for_javascript: bool = False) -> Any:
+ type_of_data = type(data)
+
+ if _is_obj_supported_primitive(data) or isinstance(data, list) or isinstance(data, tuple):
+ # This means that data was annotated as a Serializable, but it is a primitive or a tuple.
+ return converter.unstructure(data, unstructure_as=type_of_data)
+
+ type_of_data_with_typekey_already_added = flag_to_ignore_type_key_hooks(type_of_data) # type: ignore
+
+ # This is a hack which is necessary because cattrs does not work well with Protocols.
+ # Protocols are generic classes, but they don't have __orig_bases__, which cattrs
+ # assumes them to have.
+ if is_generic(type_of_data_with_typekey_already_added):
+ old_orig_bases = getattr(type_of_data_with_typekey_already_added, "__orig_bases__", ())
+ setattr(type_of_data_with_typekey_already_added, "__orig_bases__", old_orig_bases)
+
+ if isinstance(data, BaseModel):
+ # This is a shortcut: when you encounter a Pydantic model, just use Pydantic serialization.
+ # NOTE: currently we don't support `DONT_SERIALIZE` fields in pydantic models.
+ # so we just serialize all fields.
+ unstructured = data.model_dump(by_alias=for_javascript, mode="json")
+ else:
+ unstructured = converter.unstructure(data, unstructure_as=type_of_data_with_typekey_already_added)
+
+ assert isinstance(unstructured, dict)
+
+ if for_javascript:
+ unstructured.update({k: converter.unstructure(v) for k, v in get_serializable_properties(data).items()})
+
+ return {
+ TYPE_KEY: _type_to_string(type_of_data, fully_qualified=True),
+ **unstructured,
+ }
+
+
+# This is the predicate used in the factory functions above, so they trigger for serializable and attr classes
+# that have had their type key logic handled.
+def _should_serialize_without_type_key(t: type) -> bool:
+ is_serializable_class = _safe_issubclass(t, Serializable) or attr.has(t) or _safe_issubclass(t, BaseModel)
+ return is_serializable_class and _safe_issubclass(t, _AvoidTypeKeyLogic)
+
+
+def _should_add_type_key(t: type) -> bool:
+ is_serializable_class = _safe_issubclass(t, Serializable) or attr.has(t) or _safe_issubclass(t, BaseModel)
+ return is_serializable_class and not _safe_issubclass(t, _AvoidTypeKeyLogic)
+
+
+def _deserialize_serialized_object(data: Any, type_of_data: type, converter: Converter) -> Any:
+ if isinstance(data, list):
+ # Data is a list of objects.
+ return converter.structure(data, list[_ShouldDeserialize])
+ elif not isinstance(data, Mapping):
+ # Data is a primitive, like an integer or a string.
+ return converter.structure(data, type(data))
+ else:
+ # Data is a dictionary with a type key, representing an attrs object, a Pydantic model, or a Mapping
+ return _deserialize_using_type_marker(data, type_of_data, converter)
+
+
+def _should_deserialize_with_type_key_logic(t: type) -> bool:
+ is_type_that_should_be_deserialized = (
+ attr.has(t)
+ or _safe_issubclass(t, Serializable)
+ or _safe_issubclass(t, _ShouldDeserialize)
+ or t is Hashable
+ or _is_mapping_type(t)
+ or _safe_issubclass(t, BaseModel)
+ )
+ should_avoid_type_key_logic = _safe_issubclass(t, _AvoidTypeKeyLogic) or _safe_issubclass(
+ get_origin(t) or NoneType, _AvoidTypeKeyLogic
+ )
+ return is_type_that_should_be_deserialized and not should_avoid_type_key_logic
+
+
+def deserialized_object_violates_target_type(obj: Any, target_type: type) -> bool:
+ if target_type is _ShouldDeserialize or target_type is Serializable:
+ return False
+ if type(target_type) is TypeVar:
+ # We're not really able to check if the object is an instance of a type that's behind a TypeVar.
+ return False
+ return not isinstance(obj, get_origin(target_type) or target_type)
+
+
+# Note that expected_type_based_on_annotations may be much more vague than the actual type of the object.
+# For example: it may be Serializable, when the object is supposed to be
+# deserialized as a HammerResult. We get the real type from the "__type" key.
+def _deserialize_using_type_marker(
+ obj: Mapping[Any, Any],
+ expected_type_based_on_annotations: type[T],
+ converter: Converter,
+) -> T:
+ if TYPE_KEY in obj:
+ type_of_obj = _type_from_string(obj[TYPE_KEY])
+ else:
+ type_of_obj = expected_type_based_on_annotations
+
+ if _is_special_mapping_type(type_of_obj):
+ pass
+ elif _is_frozen_mapping_type(type_of_obj):
+ obj.pop(TYPE_KEY, None) # type: ignore
+ type_of_obj = FrozenMapping[_ShouldDeserialize, _ShouldDeserialize]
+ elif _is_mapping_type(type_of_obj):
+ obj.pop(TYPE_KEY, None) # type: ignore
+ type_of_obj = dict[_ShouldDeserialize, _ShouldDeserialize]
+ elif _safe_issubclass(type_of_obj, BaseModel):
+ assert isinstance(obj, dict)
+ obj.pop(TYPE_KEY, None)
+ return cast(T, type_of_obj.model_validate(obj))
+ elif not attr.has(type_of_obj):
+ # This happens when there is a primitive object which is annotated as Serializable.
+ return converter.structure(obj, type_of_obj) # type: ignore
+
+ # By mixing in the "avoid type key logic" class, force cattrs to do its normal behavior.
+ ret: T = converter.structure(obj, flag_to_ignore_type_key_hooks(type_of_obj))
+
+ if inspect.isclass(type_of_obj):
+ # Upcast the result so that it has the correct type again, without the mixin.
+ object.__setattr__(ret, "__class__", type_of_obj)
+
+ if deserialized_object_violates_target_type(ret, expected_type_based_on_annotations):
+ raise TypeError(
+ f"Tried to deserialize into type {expected_type_based_on_annotations}, but got object of type {type(ret)}"
+ )
+
+ return ret
+
+
+def _resolve_default(default: Any) -> Any:
+ if isinstance(default, attr.Factory): # type: ignore
+ return default.factory()
+ return default
+
+
+def _serialize_with_defaults(cls: type, converter: Converter) -> Callable[[Any], Any]:
+ # Handle a pydantic model
+ if _safe_issubclass(cls, BaseModel):
+ return lambda x: {k: converter.unstructure(v) for k, v in get_pydantic_model_attributes(x).items()}
+
+ members_with_defaults = get_serialize_with_default_member_names_of_type(cls)
+ overriden_kwargs = {
+ name: override(unstruct_hook=(lambda _, value=_resolve_default(default): value)) # type: ignore
+ for name, default in members_with_defaults.items()
+ }
+ return make_dict_unstructure_fn(cls, converter, **overriden_kwargs) # type: ignore
+
+
+def _should_serialize_as_serialized_exception(t: type) -> bool:
+ return (
+ _safe_issubclass(get_origin(t) or t, BaseException) and not attr.has(t) and not _safe_issubclass(t, BaseModel)
+ )
+
+
+##########################################################################################
+# CONVERTER FACTORY
+##########################################################################################
+
+
+class _ConverterFactory:
+ """Factory for creating converters with different configurations.
+
+ e.g. for serializing to javascript, or python, or to include do-not-serialize fields.
+ """
+
+ def build_base_converter(self) -> Converter:
+ # Builds of new base converter object, which registers all the hooks that are common to all converters.
+ # The idea being that all new converters start from this base and then override hooks they need to change
+ # NOTE: we need to generate a new converter object for each independent concrete converter (as opposed to
+ # using converter.copy()) since we use partial functions/closures and this way we ensure the function is
+ # being called with the correct converter object.
+ converter = Converter()
+
+ converter.register_structure_hook_func(_is_mapping_type, partial(_deserialize_mapping, converter=converter))
+ # serialization of mapping types depends on the specific converter so is done in the get_converter factory method
+
+ converter.register_unstructure_hook(frozenset, partial(_serialize_frozen_set, converter=converter))
+ converter.register_structure_hook(frozenset, partial(_deserialize_frozen_set, converter=converter))
+
+ converter.register_unstructure_hook(UUID, _serialize_uuid)
+ converter.register_structure_hook(UUID, _deserialize_uuid)
+
+ converter.register_unstructure_hook(URL, _serialize_url)
+ converter.register_structure_hook(URL, _deserialize_url)
+
+ converter.register_unstructure_hook(Decimal, _serialize_decimal)
+ converter.register_structure_hook(Decimal, _deserialize_decimal)
+
+ converter.register_unstructure_hook(FixedTraceback, _serialize_traceback)
+ converter.register_structure_hook(FixedTraceback, _deserialize_traceback)
+
+ converter.register_unstructure_hook(Path, _serialize_path)
+ converter.register_structure_hook(Path, _deserialize_path)
+
+ converter.register_unstructure_hook(anyio.Path, _serialize_anyio_path)
+ converter.register_structure_hook(anyio.Path, _deserialize_anyio_path)
+
+ converter.register_unstructure_hook(datetime.datetime, _serialize_datetime)
+ converter.register_structure_hook(datetime.datetime, _deserialize_datetime)
+
+ converter.register_unstructure_hook(bytes, _serialize_bytes)
+ converter.register_structure_hook(bytes, _deserialize_bytes)
+
+ converter.register_unstructure_hook(PosixPath, _serialize_path)
+ converter.register_structure_hook(PosixPath, _deserialize_path)
+
+ converter.register_unstructure_hook_func(_is_forward_ref, partial(_serialize_forward_ref, converter=converter))
+ converter.register_structure_hook_func(_is_forward_ref, partial(_deserialize_forward_ref, converter=converter))
+
+ converter.register_structure_hook_func(_is_union_type, partial(_deserialize_union_type, converter=converter))
+
+ converter.register_structure_hook(NoneType, lambda data, _: None)
+
+ converter.register_unstructure_hook(Enum, partial(_serialize_enum, converter=converter))
+ converter.register_structure_hook(Enum, _deserialize_enum)
+
+ converter.register_unstructure_hook_func(
+ _should_serialize_as_serialized_exception,
+ lambda e: serialize_to_dict(
+ SerializedException.build(e),
+ use_defaults_for_unserializable_fields=True,
+ ),
+ )
+
+ converter.register_structure_hook_func(
+ _should_deserialize_with_type_key_logic,
+ partial(_deserialize_serialized_object, converter=converter),
+ )
+
+ converter.register_structure_hook_func(
+ lambda t: isinstance(t, TypeVar),
+ partial(_deserialize_serialized_object, converter=converter),
+ )
+
+ return converter
+
+ def get_converter_with_defaults(self, converter: Converter) -> Converter:
+ converter.register_unstructure_hook(asyncio.Lock, lambda _: None)
+ converter.register_structure_hook(asyncio.Lock, lambda data, _: asyncio.Lock())
+
+ converter.register_unstructure_hook(asyncio.Task, lambda _: None)
+ converter.register_structure_hook(asyncio.Task, lambda data, _: None)
+
+ converter.register_unstructure_hook(asyncio.Queue, lambda _: None)
+ converter.register_structure_hook(asyncio.Queue, lambda data, _: None)
+
+ converter.register_unstructure_hook(asyncio.Event, lambda _: None)
+ converter.register_structure_hook(asyncio.Event, lambda data, _: None)
+
+ converter.register_unstructure_hook(asyncio.Semaphore, lambda _: None)
+ converter.register_structure_hook(asyncio.Semaphore, lambda data, _: None)
+
+ converter.register_unstructure_hook(abc.ABCMeta, lambda _: None)
+ converter.register_structure_hook(abc.ABCMeta, lambda data, _: None)
+ converter.register_unstructure_hook_factory(
+ _should_serialize_without_type_key,
+ partial(_serialize_with_defaults, converter=converter),
+ )
+
+ return converter
+
+ @functools.cache
+ def get_converter(
+ self,
+ for_javascript: bool = False,
+ exclude_dont_serialize_fields: bool = False,
+ use_defaults_for_unserializable_fields: bool = False,
+ ) -> Converter:
+ """Returns a converter with the given configuration.
+
+ The result of this method is cached, so subsequent calls with the same arguments will return the same converter.
+ """
+ assert not (
+ exclude_dont_serialize_fields and use_defaults_for_unserializable_fields
+ ), f"Expected exactly one flag to be set, got {exclude_dont_serialize_fields=}, {use_defaults_for_unserializable_fields=}"
+
+ converter = self.build_base_converter()
+ if for_javascript:
+ converter.register_unstructure_hook_func(
+ _is_mapping_type,
+ partial(_serialize_mapping_to_json_dict, converter=converter),
+ )
+ else:
+ converter.register_unstructure_hook_func(_is_mapping_type, partial(_serialize_mapping, converter=converter))
+ converter.register_unstructure_hook(tuple, partial(_serialize_tuple, converter=converter))
+ converter.register_structure_hook(tuple, partial(_deserialize_tuple, converter=converter))
+
+ if exclude_dont_serialize_fields:
+ converter.register_unstructure_hook_factory(
+ _should_serialize_without_type_key,
+ partial(
+ _serialize_attr_class_without_dont_serialize_fields,
+ converter=converter,
+ is_camel_case=for_javascript,
+ ),
+ )
+ else:
+ converter.register_unstructure_hook_factory(
+ _should_serialize_without_type_key,
+ partial(_serialize_attr_class_factory, converter=converter),
+ )
+ if use_defaults_for_unserializable_fields:
+ converter = self.get_converter_with_defaults(converter)
+
+ converter.register_unstructure_hook_func(
+ _should_add_type_key,
+ partial(
+ _serialize_with_type_key,
+ converter=converter,
+ for_javascript=for_javascript,
+ ),
+ )
+
+ return converter
+
+
+CONVERTER_FACTORY = _ConverterFactory()
+
+
+##########################################################################################
+# ENTRY POINTS
+##########################################################################################
+
+
+def _serialize_to_json_dumpable_object(
+ obj: Any,
+ is_reversible: bool = True,
+ for_javascript: bool = False,
+ exclude_dont_serialize_fields: bool = False,
+ use_defaults_for_unserializable_fields: bool = False,
+) -> Any:
+ if exclude_dont_serialize_fields:
+ # Check and raise error to make it clear to the caller that the object cannot be deserialized.
+ # This is a sanity check, to make it easier to debug when using do-not-serialize fields.
+ # NOTE: this will only catch cases where non-serializable fields are in obj, but not cases where
+ # the non-serializable fields are in nested objects, checking for the nested case is a little complicated
+ # so we don't do it basically.
+ assert (
+ not is_reversible
+ ), "Cannot deserialize object when excluding do-not-serialize fields (i.e. when `exclude_dont_serialize_fields=True`). If you want to serialize an object and exclude do-not-serialize fields, make sure to set `is_reversible=False`."
+
+ if use_defaults_for_unserializable_fields:
+ # The point of the use_defaults_for_unserializable_fields flag is to make it possible to serialize objects
+ # and then recreate them later even if certain fields are not fully saved. We never want to use this flag
+ # with `is_reversible=False` since we won't know the type to be able to recreate the object.
+ assert is_reversible, "Cannot restructure inputs if is_reversible=False"
+
+ # TODO: this is a hack to make it possible to serialize ExecutionContexts for class methods.
+ # This lets us serialize ExecutionContexts for calls to class methods without serializing the class itself.
+ # The long-term solution is to write a custom hook that can serialize type objects.
+ if type(obj) is dict and "__class__" in obj:
+ del obj["__class__"]
+
+ converter = CONVERTER_FACTORY.get_converter(
+ for_javascript=for_javascript,
+ exclude_dont_serialize_fields=exclude_dont_serialize_fields,
+ use_defaults_for_unserializable_fields=use_defaults_for_unserializable_fields,
+ )
+
+ dict_result = converter.unstructure(obj)
+ if for_javascript:
+ dict_result = _camelize_keys_which_represent_python_names(dict_result)
+
+ if not is_reversible:
+ return _to_json_dumpable_object_without_type_keys(dict_result)
+
+ return dict_result
+
+
+def serialize_to_dict(
+ obj: Any,
+ is_reversible: bool = True,
+ for_javascript: bool = False,
+ exclude_dont_serialize_fields: bool = False,
+ use_defaults_for_unserializable_fields: bool = False,
+) -> dict[str, Any]:
+ """Serialize to a python dict."""
+ return cast(
+ dict[str, Any],
+ _serialize_to_json_dumpable_object(
+ obj,
+ is_reversible=is_reversible,
+ for_javascript=for_javascript,
+ exclude_dont_serialize_fields=exclude_dont_serialize_fields,
+ use_defaults_for_unserializable_fields=use_defaults_for_unserializable_fields,
+ ),
+ )
+
+
+def serialize_to_json(
+ obj: Any,
+ indent: int | None = None,
+ sort_keys: bool = False,
+ is_reversible: bool = True,
+ for_javascript: bool = False,
+ exclude_dont_serialize_fields: bool = False,
+ use_defaults_for_unserializable_fields: bool = False,
+) -> str:
+ """Serialize an object to a JSON string.
+
+ This is the main serialization entrypoint.
+
+ `is_reversible` controls whether we enforce that the result can be deserialized. In some cases we don't care about
+ reversibility, e.g. when serializing data for a frontend we often don't care whether we can deserialize.
+
+ `for_javascript` controls whether we use camelCase for keys that originally were Python identifiers.
+
+ `exclude_dont_serialize_fields` controls whether we include do-not-serialize fields in the serialization.
+ If this is `False` then any attr class fields marked with as don't serialize, e.g. with `attr.ib(metadata=DONT_SERIALIZE)`,
+ will still be included in the serialization. If this is `True` then they will be excluded, however this also means that
+ the result will not be reversible (and thus the caller will have to set `is_reversible=False`).
+
+ `use_defaults_for_unserializable_fields` controls whether we fill fields that cannot be serialized with their default values.
+ IMPORTANT: If you use this flag, data may be discarded during deserialization.
+ The goal is to be able to deserialize fields to the original type without caring about the data contained.
+ Default value choices (guided by crafty serialization requirements):
+ - Fields that are marked with attr.ib(metadata=SERIALIZE_WITH_DEFAULT) have the following default values:
+ - Fields that are marked with `attr.ib(default=...)` or `attr.ib(factory=...)` use their default values.
+ - Fields that do not have a default value are filled with None.
+ - Asyncio objects are filled with None.
+ - Exceptions are replaced with a string representation
+ """
+ try:
+ unstructured = _serialize_to_json_dumpable_object(
+ obj,
+ is_reversible=is_reversible,
+ for_javascript=for_javascript,
+ exclude_dont_serialize_fields=exclude_dont_serialize_fields,
+ use_defaults_for_unserializable_fields=use_defaults_for_unserializable_fields,
+ )
+ return json.dumps(unstructured, indent=indent, sort_keys=sort_keys)
+ except Exception as e:
+ raise SerializationError(str(e)) from e
+
+
+def deserialize_from_json(
+ data: str,
+ for_javascript: bool = False,
+ exclude_dont_serialize_fields: bool = False,
+ use_defaults_for_unserializable_fields: bool = False,
+) -> Any:
+ try:
+ converter = CONVERTER_FACTORY.get_converter(
+ for_javascript=for_javascript,
+ exclude_dont_serialize_fields=exclude_dont_serialize_fields,
+ use_defaults_for_unserializable_fields=use_defaults_for_unserializable_fields,
+ )
+ return _deserialize_serialized_object(json.loads(data), _ShouldDeserialize, converter=converter)
+ except Exception as e:
+ raise SerializationError(str(e)) from e
+
+
+def deserialize_from_dict(
+ data: dict[str, Any],
+ as_type: type = _ShouldDeserialize,
+ for_javascript: bool = False,
+ exclude_dont_serialize_fields: bool = False,
+ use_defaults_for_unserializable_fields: bool = False,
+) -> Any:
+ try:
+ converter = CONVERTER_FACTORY.get_converter(
+ for_javascript=for_javascript,
+ exclude_dont_serialize_fields=exclude_dont_serialize_fields,
+ use_defaults_for_unserializable_fields=use_defaults_for_unserializable_fields,
+ )
+ return _deserialize_using_type_marker(data, as_type, converter=converter)
+ except Exception as e:
+ raise SerializationError(str(e)) from e
diff --git a/imbue_core/imbue_core/common.py b/vet/imbue_core/common.py
diff --git a/imbue_core/imbue_core/conftest.py b/vet/imbue_core/conftest.py
diff --git a/vet/imbue_core/data_types.py b/vet/imbue_core/data_types.py
@@ -0,0 +1,317 @@
+"""
+Interfaces and data types for the issue identification system.
+
+We foresee two basic kinds of issue identifiers:
+
+ 1. Heavily specialized ones.
+ - Those would typically only check for a single well-defined issue.
+ - The user only wants to know if a specific thing is wrong.
+ 2. General ones.
+ - They would still have a certain focus but they would typically check for a broader range of issues.
+ - We wouldn't know the whole range of possible issues in advance.
+ - Here, the user asks for up to N most problematic issues of the given kind in a given scope.
+
+Issue identifiers can either be created:
+ - by implementing the IssueIdentifier protocol in an arbitrary way
+ - or by leaning on a common LLM-based zero-shot classifier
+ - This has the advantage of being more efficient.
+ - We can ask for a (set of) score(s) on various metrics / error types for a list of scopes.
+ - These computations can basically all be batched together into a single call to the LLM.
+ - NOTE: as of writing this, this hasn't been implemented yet.
+
+What follows is a list of possible issues we may eventually want to identify:
+
+
+- docstrings / documentation / tests / constraints / validation
+ - missing
+ - outdated
+ - ambiguous
+ - poorly written / duplicated
+ - conflicting
+ - insufficient
+- assumptions
+ - unstated
+ - violated
+ - conflicting
+- possible race condition
+- overly complex code
+- code in need of refactoring
+- project layout in need of refactoring
+- duplicated code
+- brittle logic
+- use of state (at all, where unnecessary, where needless)
+- gross inefficiency
+- caching (the presence of, at all)
+- bad/confusing/unclear naming
+- forbidden stylistic patterns (that cannot be caught by ratchets)
+- poorly handled edge cases
+- just plain ol bugs, overall correctness, etc
+- missing test coverage (not line based, but more meaning based, esp around integration tests)
+- disagreeing ensembles
+- missing features / implementations / etc
+- refactoring elements that were missed
+- invocation outputs that seem suspect
+- overly broad types
+- misunderstanding the users mental model
+- architectural flaws
+- general critiques
+- better alternatives
+- reinvented wheels / places where some library or external service should have been used
+- mutated globals / global state / imported globals
+- any unnecessary complexity
+
+There are also things we explicitly don't want to catch with this system:
+
+- runtime errors (when running a main script or tests)
+- most ratchet errors
+- anything caught by an existing tool (typing, pylint, tests, etc)
+- errors during the deployment process itself
+- errors when building the image
+- errors about packaging, installation, dependencies, etc
+- errors collected from production
+
+"""
+
+from enum import StrEnum
+from typing import Literal
+
+from pydantic import Field
+
+from vet.imbue_core.common import generate_id
+from vet.imbue_core.pydantic_serialization import SerializableModel
+
+# Define semantics for the normalized confidence and severity scores.
+CONFIDENCE_CERTAINLY_FINE = (0.0, 0.2)
+CONFIDENCE_RATHER_FINE = (0.2, 0.4)
+CONFIDENCE_NOT_SURE = (0.4, 0.6)
+CONFIDENCE_RATHER_PROBLEMATIC = (0.6, 0.8)
+CONFIDENCE_CERTAINLY_PROBLEMATIC = (0.8, 1.0)
+
+
+class ConfidenceScore(SerializableModel):
+ """
+ A score for the confidence in the issue / error detection.
+
+ - The raw score is the score as output by the underlying model.
+ - The normalized score is rescaled in such a way that the interval between 0 and 1 maps to the defined confidence levels.
+
+ """
+
+ raw: float
+ normalized: float
+
+
+class SeverityScore(SerializableModel):
+ """
+ A score for the severity of the issue / error.
+
+ - The raw score is the score as potentially output by the underlying model.
+ - The normalized score is rescaled in such a way that the interval between 0 and 1 maps to the defined severity levels.
+
+ """
+
+ raw: float
+ normalized: float
+
+
+class LineRange(SerializableModel):
+ start: int
+ end: int
+
+ def __lt__(self, other: "LineRange") -> bool:
+ if self.start != other.start:
+ return self.start < other.start
+ return self.end < other.end
+
+ @classmethod
+ def build_from_substring(cls, file_contents: str, substring: str) -> tuple["LineRange", ...]:
+ """
+ Convert a substring in a file to a tuple of LineRange instances.
+
+ Each LineRange instance corresponds to a single occurrence of the substring in the file.
+ (Except when multiple occurences are on the same line, in which case only one LineRange is
+ created to represent them).
+
+ LineRanges are returned in the order they appear in the file.
+
+ In case the substring can't be found, an empty tuple is returned.
+
+ """
+
+ line_ranges = set()
+ offset_chars = 0
+ offset_lines = 0
+ while True:
+ cut_contents = file_contents[offset_chars:]
+ start_index = cut_contents.find(substring)
+ if start_index == -1:
+ break
+ end_index = start_index + len(substring)
+ line_start = offset_lines + cut_contents.count("\n", 0, start_index)
+ line_end = offset_lines + cut_contents.count("\n", 0, end_index)
+ offset_chars += end_index
+ offset_lines = line_end
+ line_ranges.add(LineRange(start=line_start, end=line_end))
+ return tuple(sorted(line_ranges))
+
+
+class AgenticPhase(StrEnum):
+ """Phases of agentic analysis."""
+
+ ISSUE_IDENTIFICATION = "issue_identification"
+ COLLATION = "collation"
+ FILTRATION = "filtration"
+ DEDUPLICATION = "deduplication"
+
+
+class IssueIdentifierType(StrEnum):
+ BATCHED_COMMIT_CHECK = "batched_commit_check"
+ CORRECTNESS_COMMIT_CLASSIFIER = "correctness_commit_classifier"
+ AGENTIC_ISSUE_IDENTIFIER = "agentic_issue_identifier"
+ CONVERSATION_HISTORY_IDENTIFIER = "conversation_history_issue_identifier"
+
+
+class IssueCode(StrEnum):
+ """
+ A code for the type of issue / error detected.
+
+ The code can either correspond something very specific (e.g. "ambiguous_docstring")
+ or to something more general (e.g. "function_implementation").
+
+ The latter case would be used as an "umbrella" code in cases we don't know what exactly comes out of an issue verifier.
+
+ """
+
+ # Verifier-based.
+ INCORRECT_FUNCTION_IMPLEMENTATION = "incorrect_function_implementation"
+
+ # Batched file checks
+ INEFFICIENT_CODE = "inefficient_code"
+ BAD_NAMING = "bad_naming"
+ POOR_DOCSTRING = "poor_docstring"
+ RACE_CONDITION = "race_condition"
+ HARDCODED_SECRET = "hardcoded_secret"
+ DUPLICATE_CODE = "duplicate_code"
+ UNUSED_CODE = "unused_code"
+ COMMIT_MESSAGE_MISMATCH = "commit_message_mismatch"
+
+ # Batched commit checks
+ INCOMPLETE_INTEGRATION_WITH_EXISTING_CODE = "incomplete_integration_with_existing_code"
+ DOCUMENTATION_IMPLEMENTATION_MISMATCH = "documentation_implementation_mismatch"
+ USER_REQUEST_ARTIFACTS_LEFT_IN_CODE = "user_request_artifacts_left_in_code"
+ POOR_NAMING = "poor_naming"
+ REPETITIVE_OR_DUPLICATE_CODE = "repetitive_or_duplicate_code"
+ REFACTORING_NEEDED = "refactoring_needed"
+ TEST_COVERAGE = "test_coverage"
+ RESOURCE_LEAKAGE = "resource_leakage"
+ DEPENDENCY_MANAGEMENT = "dependency_management"
+ INSECURE_CODE = "insecure_code"
+ CORRECTNESS_SYNTAX_ISSUES = "correctness_syntax_issues"
+ FAILS_SILENTLY = "fails_silently"
+ INSTRUCTION_FILE_DISOBEYED = "instruction_file_disobeyed"
+ ABSTRACTION_VIOLATION = "abstraction_violation"
+
+ # Correctness commit classifier
+ LOGIC_ERROR = "logic_error"
+ RUNTIME_ERROR_RISK = "runtime_error_risk"
+ INCORRECT_ALGORITHM = "incorrect_algorithm"
+ ERROR_HANDLING_MISSING = "error_handling_missing"
+ ASYNC_CORRECTNESS = "async_correctness"
+ TYPE_SAFETY_VIOLATION = "type_safety_violation"
+
+ # Conversation history identifier
+ MISLEADING_BEHAVIOR = "misleading_behavior"
+ INSTRUCTION_TO_SAVE = "instruction_to_save"
+
+ # Github dataset, not yet implemented in commit checks
+ MISMATCHED_CODE_PATTERNS = "mismatched_code_patterns"
+
+ # Issue code for flagging suggested improvements or new features, as opposed to actual issues
+ SUGGESTED_IMPROVEMENT = "suggested_improvement"
+
+ # Catchall
+ MISCELLANEOUS = "miscellaneous"
+ ALL_CODE_ISSUES = "all_code_issues"
+
+ # Deprecated
+ _DEPRECATED_LLM_ARTIFACTS_LEFT_IN_CODE = "llm_artifacts_left_in_code"
+
+
+class IssueLocation(SerializableModel):
+ """A location in a file."""
+
+ line_start: int
+ line_end: int
+ filename: str | None = None
+ # The scope of the issue. Usually the qualified name of the function that the issue is located in.
+ # If the issue is part of a class definition (and not limited to a particular method),
+ # the name of the class. If the issue is at the global file level, None.
+ scope: str | None = None
+
+
+IssueID = str
+
+
+class IdentifiedVerifyIssue(SerializableModel):
+ """An identified code issue / error."""
+
+ issue_id: IssueID | None = Field(default_factory=generate_id)
+ code: IssueCode
+ description: str
+ severity_score: SeverityScore
+ location: tuple[IssueLocation, ...] = Field(default_factory=tuple)
+ confidence_score: ConfidenceScore | None = None
+ fix: str | None = None
+ violating_instruction: str | None = None
+ violating_instruction_location: IssueLocation | None = None
+ # TODO: remove these fields
+
+ # An issue is fundamentally fixable if we can change the implementation to make the issue go away.
+ # (An example of a non-fixable issue is a nonsensical commit message - changing the implementation doesn't help here.)
+ # - iffv something is not fixable why would we want to report it?
+ is_fixable: bool = True
+
+
+class InvocationInfo(SerializableModel):
+ """Information about an LLM invocation including token usage, timing, and cost. Populate whichever fields are available."""
+
+ input_tokens: int | None = None
+ cache_creation_input_tokens: int | None = None
+ cache_read_input_tokens: int | None = None
+ total_input_tokens: int | None = None
+ output_tokens: int | None = None
+ duration_ms: float | None = None
+ cost: float | None = None
+ num_turns: int | None = None
+
+
+class IssueIdentificationLLMResponseMetadata(SerializableModel):
+ """Configuration metadata for LLM responses."""
+
+ type: Literal["IssueIdentificationLLMResponseMetadata", "IssueIdentificationLLMResponseConfig"] = (
+ "IssueIdentificationLLMResponseMetadata"
+ )
+ agentic_phase: AgenticPhase | None = None
+ issue_type: IssueCode | None = None
+ identifier_name: str | None = None
+ issue_ids: tuple[IssueID] | None = None
+
+
+class LLMResponse(SerializableModel):
+ metadata: IssueIdentificationLLMResponseMetadata # Make this a union if there are other types of LLM responses
+ raw_response: tuple[str, ...]
+ invocation_info: InvocationInfo | None = None
+
+ # Deprecated fields
+ config: IssueIdentificationLLMResponseMetadata | None = Field(default=None, deprecated=True)
+
+
+class IssueIdentificationDebugInfo(SerializableModel):
+ llm_responses: tuple[LLMResponse, ...]
+
+
+class IssueIdentifierResult(SerializableModel):
+ """Container for an identified issue along with the LLM responses that generated it."""
+
+ issue: IdentifiedVerifyIssue
+ passes_filtration: bool = True
diff --git a/imbue_core/imbue_core/errors.py b/vet/imbue_core/errors.py
diff --git a/imbue_core/imbue_core/fixed_traceback.py b/vet/imbue_core/fixed_traceback.py
diff --git a/imbue_core/imbue_core/frozen_utils.py b/vet/imbue_core/frozen_utils.py
diff --git a/vet/imbue_core/itertools.py b/vet/imbue_core/itertools.py
@@ -0,0 +1,52 @@
+import contextlib
+import itertools
+from typing import Generator
+from typing import Iterable
+from typing import Sequence
+from typing import TypeVar
+
+from vet.imbue_core.errors import ImbueError
+
+T = TypeVar("T")
+
+
+class ImbueItertoolsValueError(ImbueError, ValueError):
+ """This value error is thrown when the assumptions of the itertools module are violated."""
+
+
+def flatten(iterable: Iterable[Iterable[T]]) -> list[T]:
+ return list(itertools.chain.from_iterable(iterable))
+
+
+def remove_none(data: Iterable[T | None]) -> list[T]:
+ return [x for x in data if x is not None]
+
+
+def only(x: Iterable[T]) -> T:
+ try:
+ (value,) = x
+ except ValueError as e:
+ message = "Expected exactly one value"
+ if isinstance(x, Sequence):
+ with contextlib.suppress():
+ message += f" but got {len(x)} {x[:3]=}"
+ raise ImbueItertoolsValueError(message) from e
+
+ return value
+
+
+def first(iterable: Iterable[T]) -> T | None:
+ return next(iter(iterable), None)
+
+
+# TODO replace with itertools.batched when we can require Python 3.12+
+def generate_chunks(iterable: Iterable[T], chunk_size: int) -> Generator[tuple[T, ...], None, None]:
+ """Yield successive n-sized chunks from any iterable"""
+ chunk = []
+ for item in iterable:
+ chunk.append(item)
+ if len(chunk) == chunk_size:
+ yield tuple(chunk)
+ chunk = []
+ if len(chunk) > 0:
+ yield tuple(chunk)
diff --git a/imbue_core/imbue_core/language_model_mode.py b/vet/imbue_core/language_model_mode.py
diff --git a/vet/imbue_core/nested_evolver.py b/vet/imbue_core/nested_evolver.py
@@ -0,0 +1,220 @@
+"""Evolver uses duck-typing to give the appearance of editing a frozen, nested structure of attrs classes and tuples, recording changes in a way that they can be applied to generate a newly frozen instance.
+
+One of the design goals is that mypy, autocomplete, and automatic refactoring work for the assignments made into these nested structures.
+
+If you make changes here and then the tests fail with:
+```
+E RecursionError: maximum recursion depth exceeded
+!!! Recursion detected (same locals & position)
+```
+It's possible that you're accidentally invoking `Evolver.something_undefined` and that's causing the infinite recursion.
+Mypy cannot catch this when it thinks the type is an `Evolver` because the `Evolver` class has a `__getattr__` method that makes it look like any attribute access could be valid.
+"""
+
+import threading
+from typing import Any
+from typing import Callable
+from typing import Generic
+from typing import TypeVar
+from typing import cast
+
+import attr
+from pydantic import BaseModel
+
+from vet.imbue_core.frozen_utils import FrozenDict
+from vet.imbue_core.pydantic_utils import model_update
+
+_T = TypeVar("_T")
+
+_threading_local = threading.local()
+
+
+def evolver(obj: _T) -> _T:
+ """Creates a wrapper around an immutable attrs object, tuple, or FrozenDict that records potentially nested attribute assignments.
+
+ The return type is our first white lie to the type system.
+ """
+ result = _Evolver[_T](obj)
+ # The cast is a little white lie to the type system to make type-checking, autocomplete, and refactoring work.
+ return cast(_T, result)
+
+
+def assign(dest: _T, src: Callable[[], _T]) -> None:
+ """Since mypy would complain about assignments to frozen attrs fields, use this function to make assignments.
+
+ The only reason src is a `Callable[[], _T]` instead of just `_T` is that it makes type checking signal attempts to assign
+ the wrong type to the field. Surprisingly, just using `(dest: _T, src: _T)` doesn't cause mypy to complain about type mismatch.
+ """
+ assert isinstance(dest, _Evolver) # Tricked you, type system!
+ dest_evolver: _Evolver[_T] = cast(_Evolver[_T], dest)
+ dest_evolver.assign(src())
+
+
+def chill(evolver: _T) -> _T:
+ """Produces a new frozen instance with the recorded changes applied.
+
+ The name `chill` is a play on the fact that original input was frozen, and we are now re-freezing it.
+ """
+ assert isinstance(evolver, _Evolver) # Tricked you, type system!
+ cast_evolver = cast(_Evolver[_T], evolver)
+ return cast_evolver.chill()
+
+
+class _RegularValue:
+ regular_value: Any
+
+ def __init__(self, value: Any) -> None:
+ self.regular_value = value
+
+
+class _AttrValue:
+ attr_value: Any
+ child_evolver_by_name: dict[str, "_Evolver[Any]"]
+
+ def __init__(self, value: Any) -> None:
+ self.attr_value = value
+ self.child_evolver_by_name = {}
+
+
+class _PydanticModelValue:
+ pydantic_model_value: Any
+ child_evolver_by_name: dict[str, "_Evolver[Any]"]
+
+ def __init__(self, value: Any) -> None:
+ self.pydantic_model_value = value
+ self.child_evolver_by_name = {}
+
+
+class _TupleValue:
+ tuple_evolvers: list["_Evolver[Any]"]
+
+ def __init__(self, value: tuple[Any, ...]) -> None:
+ # It may be premature to create evolvers for all the elements of the tuple, but it's easier.
+ self.tuple_evolvers = [evolver(item) for item in value]
+
+
+class _FrozenDictValue:
+ frozen_dict_evolvers: dict[Any, "_Evolver[Any]"]
+
+ def __init__(self, value: dict[Any, Any]) -> None:
+ # It may be premature to create evolvers for all the elements of dict, but it's easier.
+ self.frozen_dict_evolvers = {k: evolver(v) for k, v in value.items()}
+
+
+class _Evolver(Generic[_T]):
+ # pyre-ignore[13]: pyre is confused by the trickery here
+ _value: (
+ _RegularValue
+ | _AttrValue
+ | _TupleValue
+ | _FrozenDictValue
+ | _PydanticModelValue
+ )
+
+ def __init__(self, initial_value: _T) -> None:
+ super().__init__()
+ self.assign(initial_value)
+
+ def assign(self, new_value: _T) -> None:
+ """Assign a new value to this Evolver, recording a change to the frozen structure to be later applied during `chill()`."""
+
+ if attr.has(type(new_value)):
+ self._value = _AttrValue(new_value)
+ elif isinstance(new_value, BaseModel):
+ self._value = _PydanticModelValue(new_value)
+ elif isinstance(new_value, tuple):
+ self._value = _TupleValue(new_value)
+ elif isinstance(new_value, FrozenDict):
+ self._value = _FrozenDictValue(new_value)
+ else:
+ self._value = _RegularValue(new_value)
+
+ def __getattr__(self, item: str) -> "_Evolver[Any]":
+ """Access Evolvers for nested members of a frozen attrs object."""
+ try:
+ value = self._value
+ if isinstance(value, _AttrValue):
+ if item not in value.child_evolver_by_name:
+ child_obj = getattr(value.attr_value, item)
+ result = evolver(child_obj)
+ assert isinstance(result, _Evolver), (
+ "Expose a lie to the type system."
+ )
+ value.child_evolver_by_name[item] = result
+ return value.child_evolver_by_name[item]
+ elif isinstance(value, _PydanticModelValue):
+ if item not in value.child_evolver_by_name:
+ child_obj = getattr(value.pydantic_model_value, item)
+ result = evolver(child_obj)
+ assert isinstance(result, _Evolver), (
+ "Expose a lie to the type system."
+ )
+ value.child_evolver_by_name[item] = result
+ return value.child_evolver_by_name[item]
+ raise TypeError(
+ f"You're trying to access field {item=} on an object of {type(value)=} that doesn't have that field (should have been a mypy error)."
+ )
+ except BaseException as e:
+ if hasattr(_threading_local, "evolved_obj"):
+ # pyre-ignore[16]: pyre is suspicious of the trickery here
+ if getattr(_threading_local, "evolved_obj") == self:
+ delattr(_threading_local, "evolved_obj")
+ raise e
+
+ # TODO: It wouldn't be terribly difficult to support "appending" to the tuple as well, by appending to this list.
+ def __getitem__(self, key: Any) -> "_Evolver[Any]":
+ """Access Evolvers for the elements of a tuple or dict."""
+ value = self._value
+ if isinstance(value, _TupleValue):
+ assert isinstance(key, int)
+ return value.tuple_evolvers[key]
+ elif isinstance(value, _FrozenDictValue):
+ if key not in value.frozen_dict_evolvers:
+ # Presumably we're going to evolver_assign to this very soon.
+ cast(_FrozenDictValue, self._value).frozen_dict_evolvers[key] = (
+ _Evolver(_RegularValue(None))
+ )
+ return cast(_FrozenDictValue, self._value).frozen_dict_evolvers[key]
+ raise TypeError(
+ f"You're using [square_brackets] access {key=} on an object of {type(self._value)=} that doesn't support this (should have been a mypy error)."
+ )
+
+ def chill(self) -> _T:
+ """Recursively apply the recorded changes to the original object and return a new frozen instance."""
+ if isinstance(self._value, _AttrValue):
+ new_children: dict[str, Any] = {
+ name: chill(child)
+ for name, child in self._value.child_evolver_by_name.items()
+ }
+ assert attr.has(self._value.attr_value.__class__)
+ return cast(
+ _T,
+ attr.evolve(
+ cast(Any, cast(_AttrValue, self._value).attr_value), **new_children
+ ),
+ )
+ elif isinstance(self._value, _PydanticModelValue):
+ return cast(
+ _T,
+ model_update(
+ self._value.pydantic_model_value,
+ update={
+ name: chill(child)
+ for name, child in self._value.child_evolver_by_name.items()
+ },
+ ),
+ )
+ elif isinstance(self._value, _TupleValue):
+ return cast(
+ _T, tuple(evolver.chill() for evolver in self._value.tuple_evolvers)
+ )
+ elif isinstance(self._value, _RegularValue):
+ return cast(_T, self._value.regular_value)
+ elif isinstance(self._value, _FrozenDictValue):
+ return cast(
+ _T,
+ FrozenDict(
+ {k: v.chill() for k, v in self._value.frozen_dict_evolvers.items()}
+ ),
+ )
+ raise ValueError(f"This Evolver has no value to evolve, {type(self._value)=}.")
diff --git a/imbue_core/imbue_core/py.typed b/vet/imbue_core/py.typed
diff --git a/vet/imbue_core/pydantic_serialization.py b/vet/imbue_core/pydantic_serialization.py
@@ -0,0 +1,118 @@
+import threading
+from typing import Any
+from typing import TypeVar
+from typing import cast
+
+from pydantic import BaseModel
+from pydantic import ConfigDict
+from pydantic import Discriminator
+from pydantic.alias_generators import to_camel
+
+from vet.imbue_core.nested_evolver import _Evolver
+from vet.imbue_core.nested_evolver import chill
+from vet.imbue_core.nested_evolver import evolver
+from vet.imbue_core.serialization_types import Serializable
+
+T = TypeVar("T", bound=BaseModel)
+V = TypeVar("V")
+
+_threading_local = threading.local()
+
+
+class EvolvableModel:
+ # pyre-ignore[47]: pyre is not so easily tricked
+ def evolve(self: T, attribute: V, new_value: V) -> T:
+ # pyre-ignore[16]: pyre doesn't know about evolved_obj
+ assert _threading_local.evolved_obj is not None, (
+ ".ref() must be called before evolve"
+ )
+
+ assert isinstance(attribute, _Evolver) # Tricked you, type system!
+ dest_evolver: _Evolver[T] = cast(_Evolver[T], attribute)
+ dest_evolver.assign(new_value)
+
+ result = chill(_threading_local.evolved_obj)
+ _threading_local.evolved_obj = None
+ return result
+
+ # pyre-ignore[47]: pyre is not so easily tricked
+ def ref(self: T) -> T:
+ # pyre-ignore[16]: pyre doesn't know about evolved_obj
+ _threading_local.evolved_obj = evolver(self)
+ return _threading_local.evolved_obj
+
+
+class MutableModel(BaseModel):
+ """
+ The base class for any internal data that strictly must be mutable. Should be used sparingly.
+ """
+
+ model_config = ConfigDict(
+ frozen=False,
+ extra="forbid",
+ # FIXME: go back to preventing arbitrary types once we're done converting
+ # arbitrary_types_allowed=False,
+ arbitrary_types_allowed=True,
+ )
+
+
+class SerializableModel(EvolvableModel, BaseModel, Serializable):
+ """
+ The base class for all data that can be serialized to/from JSON.
+ """
+
+ model_config = ConfigDict(
+ frozen=True,
+ ser_json_bytes="base64",
+ val_json_bytes="base64",
+ alias_generator=to_camel,
+ validate_by_alias=True,
+ validate_by_name=True,
+ # any extra values will end up in the __pydantic_extra__ field
+ # this is effectively required for backwards compatibility
+ # IMPORTANT: note that, by default, we clear this below! These types are ONLY for backwards compatibility
+ extra="allow",
+ # this is also effectively required for backwards compatibility
+ arbitrary_types_allowed=True,
+ )
+
+ # this is a place where we might way to do any backwards compatibility related logic
+ def model_post_init(self, __context: Any) -> None:
+ pydantic_extra = self.__pydantic_extra__
+ assert pydantic_extra is not None
+ pydantic_extra.clear()
+
+
+# this is mostly here for the default cases.
+# When you want to upgrade a model (and keep it backwards compatible), you can make a custom discriminator
+# (eg, that looks for the old type name or converts the old class names)
+def build_discriminator(
+ field_name: str = "object_type",
+ additional_types_and_string_representations: tuple[tuple[type, str], ...] = (),
+) -> Discriminator:
+ """
+ Build a discriminator function for a Pydantic model.
+
+ Args:
+ field_name (str): The name of the field to use as the discriminator.
+ additional_types_and_string_representations (Tuple[Tuple[Type, str], ...]): Register additional types to the discriminator.
+
+ Returns:
+ Callable[[T | dict], str]: A function that takes an instance of T or a dictionary and returns the value of the
+ specified field.
+ """
+
+ def discriminator(obj: T | dict) -> str:
+ for (
+ model_type,
+ string_representation,
+ ) in additional_types_and_string_representations:
+ if isinstance(obj, model_type):
+ return string_representation
+ if isinstance(obj, dict):
+ if field_name not in obj:
+ return obj[to_camel(field_name)]
+ return obj[field_name]
+ return getattr(obj, field_name)
+
+ return Discriminator(discriminator=discriminator)
diff --git a/imbue_core/imbue_core/pydantic_utils.py b/vet/imbue_core/pydantic_utils.py
diff --git a/imbue_core/imbue_core/secrets_utils.py b/vet/imbue_core/secrets_utils.py
diff --git a/vet/imbue_core/serialization.py b/vet/imbue_core/serialization.py
@@ -0,0 +1,424 @@
+import builtins
+import datetime
+import json
+from enum import Enum
+from functools import cached_property
+from importlib import import_module
+from importlib.metadata import version
+from pathlib import PosixPath
+from traceback import format_tb
+from types import TracebackType
+from typing import Any
+from typing import Hashable
+from typing import Iterable
+from typing import Mapping
+from typing import TypeVar
+from typing import cast
+from uuid import UUID
+
+from loguru import logger
+from typing_extensions import TypeAliasType
+from yasoo import Deserializer
+from yasoo import Serializer
+from yasoo.constants import ENUM_VALUE_KEY
+from yasoo.objects import DictWithSerializedKeys
+from yasoo.serialization import _convert_to_json_serializable
+from yasoo.utils import get_fields
+from yasoo.utils import is_obj_supported_primitive
+from yasoo.utils import normalize_type
+from yasoo.utils import resolve_types
+
+from vet.imbue_core.fixed_traceback import FixedTraceback
+from vet.imbue_core.pydantic_serialization import SerializableModel
+from vet.imbue_core.serialization_types import Serializable
+
+assert version("yasoo") == "0.12.6", (
+ "This code was written for yasoo 0.12.6 and requires inheriting / monkeypatching the deserializer, so you probably don't want to use any other version without fixing TupleDeserializer"
+)
+
+T = TypeVar("T", bound=Hashable)
+
+
+class TupleDeserializer(Deserializer):
+ def _deserialize(
+ self,
+ data: bool | int | float | str | list[Any] | dict[str, Any] | None,
+ obj_type: type[T] | None,
+ type_key: str | None,
+ allow_extra_fields: bool,
+ external_globals: dict[str, Any],
+ ignore_custom_deserializer: bool = False,
+ ) -> object:
+ all_globals = dict(globals())
+ all_globals.update(external_globals)
+ if is_obj_supported_primitive(data):
+ return data
+ if isinstance(data, list):
+ list_types = self._get_list_types(obj_type, data)
+ return tuple(
+ [
+ self._deserialize(d, t, type_key, allow_extra_fields, all_globals)
+ for t, d in list_types
+ ]
+ )
+
+ assert isinstance(data, dict), f"Expected a dict, but got {type(data)}"
+
+ # load wrapped primitives
+ if type_key is not None:
+ type_data = data.get(type_key, None)
+
+ if (
+ type_data is not None
+ and type_data.startswith("builtins.")
+ and type_data != "builtins.dict"
+ ):
+ return data["value"]
+
+ # TODO: we need to potentially handle `builtins.dict`
+ # if type_key is not None:
+ # type_data = data.get(type_key, None)
+ #
+ # # TODO: serialization currently breaks with builtin.dicts and dicts with non-string keys
+ # if type_data == "builtins.dict":
+ # raise NotImplementedError(
+ # "Only `FrozenMapping` is supported for dict serialization/deserialization, call `freeze_mapping` on your dict before serializing"
+ # )
+ # if type_data is not None and type_data.startswith("builtins.") and type_data != "builtins.dict":
+ # return data["value"]
+
+ # TODO: remove this hack. Many of our sqlite files (search s3_sqlite_path) have FrozenDicts
+ if (
+ isinstance(type_key, str)
+ and data.get(type_key, None) == "flax.core.frozen_dict.FrozenDict"
+ ):
+ data[type_key] = "imbue_core.frozen_utils.FrozenMapping"
+ # we deliberately pass in a `None` type_key sometimes, which results in just returning obj_type
+ obj_type = self._get_object_type(
+ obj_type, data, type_key, all_globals
+ ) # pyre-ignore[6]
+ if type_key in data:
+ data.pop(type_key)
+ real_type, generic_args = normalize_type(obj_type, all_globals)
+ if external_globals and isinstance(real_type, type):
+ bases = {real_type}
+ while bases:
+ all_globals.update((b.__name__, b) for b in bases)
+ bases = {ancestor for b in bases for ancestor in b.__bases__}
+
+ if not ignore_custom_deserializer:
+ deserialization_method = self._custom_deserializers.get(
+ obj_type, self._custom_deserializers.get(real_type)
+ )
+ if deserialization_method:
+ return deserialization_method(data)
+ for base_class, method in self._inheritance_deserializers.items():
+ if issubclass(real_type, base_class):
+ return method(data, real_type)
+
+ key_type = None
+ try:
+ # pyre-fixme[6]: obj_type needs to be Hashable, but pyre isn't sure that it is
+ fields = {f.name: f for f in get_fields(obj_type)}
+ except TypeError:
+ if obj_type is FixedTraceback:
+ return FixedTraceback.from_dict(data["value"])
+ if issubclass(real_type, Enum):
+ value = data[ENUM_VALUE_KEY]
+ if isinstance(value, str):
+ try:
+ return real_type[value]
+ except KeyError:
+ for e in real_type:
+ if e.name.lower() == value.lower():
+ return e
+ return real_type(value)
+ # TODO: serialization currently breaks with builtin.dicts and dicts with non-string keys
+ # if you have weird keys in your dict this branch won't be hit and your object won't be properly deserialized
+ elif issubclass(real_type, Mapping):
+ key_type = generic_args[0] if generic_args else None
+ if self._is_mapping_dict_with_serialized_keys(key_type, data):
+ # pyre-fixme[9]: obj_type needs to be Hashable, but pyre doesn't realize that type[DictWithSerializedKeys] is ok
+ obj_type = DictWithSerializedKeys
+ # pyre-fixme[6]: arg of get_fields needs to be Hashable, but pyre doesn't realize that type[DictWithSerializedKeys] is ok
+ fields = {f.name: f for f in get_fields(DictWithSerializedKeys)}
+ value_type = generic_args[1] if generic_args else Any
+ fields["data"].field_type = dict[str, value_type] # type: ignore
+ else:
+ return self._load_mapping(
+ data,
+ real_type,
+ generic_args,
+ type_key,
+ allow_extra_fields,
+ all_globals,
+ )
+ elif issubclass(real_type, Iterable):
+ # If we got here it means data is not a list, so obj_type came from the data itself and is safe to use
+ return self._load_iterable(
+ data, obj_type, type_key, allow_extra_fields, all_globals
+ )
+ elif real_type != obj_type:
+ return self._deserialize(
+ data, real_type, type_key, allow_extra_fields, external_globals
+ )
+ else:
+ raise
+
+ self._check_for_missing_fields(data, fields, obj_type)
+ self._check_for_extraneous_fields(data, fields, obj_type, allow_extra_fields)
+ self._load_inner_fields(data, fields, type_key, allow_extra_fields, all_globals)
+ if obj_type is DictWithSerializedKeys:
+ return self._load_dict_with_serialized_keys(
+ obj_type(**data), key_type, type_key, allow_extra_fields, all_globals
+ )
+ kwargs = {k: v for k, v in data.items() if fields[k].init}
+ assert obj_type is not None
+ result = obj_type(**kwargs)
+ for k, v in data.items():
+ if k not in kwargs:
+ setattr(result, k, v)
+ return result
+
+
+# TODO: probably a good idea to ensure that all dicts are frozen as well...
+class FrozenSerializer(Serializer):
+ def __init__(
+ self, force_serialization: bool, allow_unsafe_list_serialization: bool = False
+ ) -> None:
+ super().__init__()
+ self._force_serialization = force_serialization
+ self._allow_unsafe_list_serialization = allow_unsafe_list_serialization
+
+ def _serialize_iterable(
+ self,
+ obj: Iterable[object],
+ type_key: Any,
+ fully_qualified_types: Any,
+ preserve_iterable_types: Any,
+ stringify_dict_keys: Any,
+ ) -> list[object]:
+ if isinstance(obj, list):
+ if self._allow_unsafe_list_serialization:
+ logger.info("Converting list to tuple for serialization: {}", obj)
+ obj = tuple(obj)
+ else:
+ raise Exception(
+ f"Lists are not allowed for serialization. Use tuples instead. Current iterable: {obj}"
+ )
+ assert isinstance(obj, (tuple, frozenset, bytes)), (
+ f"All iterables should be tuples or frozenset. Received {obj}"
+ )
+ return cast(
+ list[object],
+ tuple(
+ self._serialize(
+ item,
+ type_key,
+ fully_qualified_types,
+ preserve_iterable_types,
+ stringify_dict_keys,
+ )
+ for item in obj
+ ),
+ )
+
+ # overriding this method just to get some better error messages out--previously it would just "type error" and
+ # moan about things like int64 not being serializable, which is fine, but it is nicer if the key is included
+ def serialize(
+ self,
+ obj: Any,
+ type_key: str | None = "__type",
+ fully_qualified_types: bool = True,
+ preserve_iterable_types: bool = False,
+ stringify_dict_keys: bool = True,
+ globals: dict[str, Any] | None = None,
+ ) -> bool | int | float | str | list | dict[str, Any] | None:
+ try:
+ if is_obj_supported_primitive(obj):
+ return obj # type: ignore
+
+ if globals:
+ self._custom_serializers = resolve_types(
+ self._custom_serializers, globals
+ ) # type: ignore
+
+ result = self._serialize(
+ obj,
+ type_key,
+ fully_qualified_types,
+ preserve_iterable_types,
+ stringify_dict_keys,
+ inner=False,
+ )
+ try:
+ result = _convert_to_json_serializable(result)
+ except TypeError:
+ _convert_to_json_serializable_with_better_errors(result)
+ assert False, "previous method should have raised..."
+ return result # type: ignore
+ except Exception:
+ if self._force_serialization:
+ return repr(obj)
+ else:
+ raise
+
+
+JsonTypeAlias = TypeAliasType(
+ "JsonTypeAlias",
+ "dict[str, JsonTypeAlias] | list[JsonTypeAlias] | str | int | float | bool | None",
+)
+
+
+class SerializedException(SerializableModel):
+ """A serializable dataclass that represents an exception"""
+
+ exception: str
+ args: "tuple[SerializedException | JsonTypeAlias, ...]" # pyre-ignore[11]: pyre doesn't like TypeAliasType
+ traceback_dict: JsonTypeAlias
+
+ @classmethod
+ def build(
+ cls, exception: BaseException, traceback: TracebackType | None = None
+ ) -> "SerializedException":
+ if traceback is None:
+ traceback = exception.__traceback__
+ assert traceback is not None, " ".join(
+ (
+ "No traceback deriveable or as a concrete argument!",
+ f"You probably want to convert_to_serialized_exception in your except clause: {exception=}",
+ )
+ )
+ return SerializedException( # pyre-fixme[28]: pyre doesn't understand pydantic
+ exception=get_fully_qualified_name_for_error(exception),
+ args=tuple(
+ _convert_serialized_exception_args(x, traceback) for x in exception.args
+ ),
+ traceback_dict=FixedTraceback.from_tb(traceback).as_dict(),
+ )
+
+
+def _convert_serialized_exception_args(
+ error: Serializable, traceback: TracebackType | None = None
+) -> JsonTypeAlias:
+ if isinstance(error, BaseException):
+ return SerializedException.build(error, traceback=traceback)
+ elif isinstance(error, (list, tuple)):
+ return tuple(_convert_serialized_exception_args(x, traceback) for x in error)
+ return error
+
+
+def get_fully_qualified_name_for_error(e: BaseException) -> str:
+ if e.__class__.__module__ == "builtins":
+ return e.__class__.__name__
+ return f"{e.__class__.__module__}.{e.__class__.__name__}"
+
+
+def _convert_to_json_serializable_with_better_errors(
+ obj: Any, path: str = ""
+) -> int | float | str | list | dict | None:
+ if is_obj_supported_primitive(obj):
+ return obj # type: ignore
+ if isinstance(obj, Mapping):
+ return {
+ key: _convert_to_json_serializable_with_better_errors(
+ value, f"{path}.{key}"
+ )
+ for key, value in obj.items()
+ }
+ if isinstance(obj, Iterable):
+ return [
+ _convert_to_json_serializable_with_better_errors(item, f"{path}[{i}]")
+ for i, item in enumerate(obj)
+ ]
+ raise TypeError(
+ f'Found object of type "{type(obj).__name__}" at {path} which cannot be serialized'
+ )
+
+
+SERIALIZER = FrozenSerializer(
+ force_serialization=False, allow_unsafe_list_serialization=False
+)
+DESERIALIZER = TupleDeserializer()
+
+# note: you cannot change this without changing other calls to yasoo, this is its default
+TYPE_KEY = "__type"
+
+
+class SerializationError(Exception):
+ pass
+
+
+@SERIALIZER.register()
+def serialize_frozen_set(data: frozenset) -> dict:
+ value = SERIALIZER.serialize(tuple(data))
+ return {"value": value}
+
+
+@DESERIALIZER.register()
+def deserialize_frozen_set(data: dict) -> frozenset:
+ return frozenset(DESERIALIZER.deserialize(data["value"], tuple))
+
+
+@SERIALIZER.register()
+def serialize_uuid(data: UUID) -> dict:
+ return {"value": data.hex}
+
+
+@DESERIALIZER.register()
+def deserialize_uuid(data: dict) -> UUID:
+ return UUID(data["value"])
+
+
+@SERIALIZER.register()
+def serialize_traceback(data: FixedTraceback) -> dict:
+ return {"value": data.to_dict()}
+
+
+@DESERIALIZER.register()
+def deserialize_traceback(data: dict) -> FixedTraceback:
+ return FixedTraceback.from_dict(data["value"])
+
+
+@SERIALIZER.register()
+def serialize_posix_path(data: PosixPath) -> dict:
+ return {"value": str(data)}
+
+
+@DESERIALIZER.register()
+def deserialize_posix_path(data: dict) -> PosixPath:
+ return PosixPath(data["value"])
+
+
+@SERIALIZER.register()
+def serialize_datetime(data: datetime.datetime) -> dict:
+ return {
+ "time": data.astimezone(datetime.timezone.utc).timestamp(),
+ "tzaware": data.tzinfo is not None,
+ "__type": "datetime.datetime",
+ }
+
+
+@DESERIALIZER.register()
+def deserialize_datetime(data: dict) -> datetime.datetime:
+ return datetime.datetime.fromtimestamp(
+ data["time"], datetime.timezone.utc if data.get("tzaware", None) else None
+ )
+
+
+def serialize_to_json(
+ obj: Any, indent: int | None = None, sort_keys: bool = False
+) -> str:
+ try:
+ return json.dumps(SERIALIZER.serialize(obj), indent=indent, sort_keys=sort_keys)
+ except Exception as e:
+ raise SerializationError(str(e)) from e
+
+
+def deserialize_from_json(data: str) -> Any:
+ try:
+ return DESERIALIZER.deserialize(
+ json.loads(data)
+ ) # pyre-ignore[20]: pyre doesn't understand deserialize
+ except Exception as e:
+ raise SerializationError(str(e)) from e
diff --git a/imbue_core/imbue_core/serialization_types.py b/vet/imbue_core/serialization_types.py
diff --git a/imbue_core/imbue_core/test_repo_utils.py b/vet/imbue_core/test_repo_utils.py
diff --git a/imbue_core/imbue_core/time_utils.py b/vet/imbue_core/time_utils.py
diff --git a/imbue_tools/imbue_tools/__init__.py b/vet/imbue_tools/__init__.py
diff --git a/vet/imbue_tools/get_conversation_history/get_conversation_history.py b/vet/imbue_tools/get_conversation_history/get_conversation_history.py
@@ -0,0 +1,70 @@
+import json
+from typing import assert_never
+
+from loguru import logger
+from pydantic import TypeAdapter
+from pydantic import ValidationError
+
+from vet.vet_types.chat_state import ContentBlockTypes
+from vet.vet_types.messages import ChatInputUserMessage
+from vet.vet_types.messages import ConversationMessageUnion
+from vet.vet_types.messages import ResponseBlockAgentMessage
+
+
+class ConversationLoadingError(Exception):
+ pass
+
+
+# === formatting for prompt ===
+
+
+def delete_unnecessary_content_block_fields(block: ContentBlockTypes) -> str:
+ """Returns the content as a json-serialized string without the fields that we don't want to include in the prompt"""
+ fields_to_remove = {"id"}
+ return block.model_dump_json(exclude=fields_to_remove)
+
+
+def delete_unnecessary_conversation_message_fields(
+ message: ConversationMessageUnion,
+) -> str:
+ """Returns the message as a json-serialized string without the fields that we don't want to include in the prompt"""
+ general_fields_to_remove = {"message_id", "source", "approximate_creation_time"}
+ match message:
+ case ChatInputUserMessage():
+ # remove the 'files' field if it's empty
+ fields_to_remove = general_fields_to_remove | {"model_name"} | {"files"} if not message.files else set()
+ return message.model_dump_json(exclude=fields_to_remove)
+ case ResponseBlockAgentMessage():
+ fields_to_remove = general_fields_to_remove | {"assistant_message_id"}
+ return json.dumps(
+ message.model_dump(mode="json", exclude=fields_to_remove)
+ | {"content": [delete_unnecessary_content_block_fields(block) for block in message.content]}
+ )
+ case _ as unreachable:
+ assert_never(unreachable)
+
+
+def format_conversation_history_for_prompt(
+ conversation_history: tuple[ConversationMessageUnion, ...],
+) -> str:
+ formatted_messages = [delete_unnecessary_conversation_message_fields(message) for message in conversation_history]
+ return "\n".join(message for message in formatted_messages if message is not None)
+
+
+# === loading from file ===
+
+
+def parse_conversation_history(
+ conversation_str: str,
+) -> tuple[ConversationMessageUnion, ...]:
+ """Load a jsonl string into a list of conversation messages"""
+ messages = []
+ for line in conversation_str.strip().splitlines():
+ try:
+ # deserialize the message with pydantic
+ message: ConversationMessageUnion = TypeAdapter(ConversationMessageUnion).validate_json(line)
+ except ValidationError:
+ logger.info("Skipping malformed history line {}", line)
+ continue
+ messages.append(message)
+ return tuple(messages)
diff --git a/vet/imbue_tools/get_conversation_history/input_data_types.py b/vet/imbue_tools/get_conversation_history/input_data_types.py
@@ -0,0 +1,72 @@
+from typing import Self
+from typing import TypeVar
+
+from pydantic import model_validator
+
+from vet.imbue_core.pydantic_serialization import SerializableModel
+from vet.vet_types.messages import ConversationMessageUnion
+
+
+class IdentifierInputsMissingError(Exception):
+ pass
+
+
+class IdentifierInputs(SerializableModel):
+ # goal (for now, commit message) and diff to check
+ maybe_goal: str | None = None
+ maybe_diff: str | None = None
+
+ # whole files to check
+ maybe_files: tuple[str, ...] | None = None
+
+ # conversation history to check
+ maybe_conversation_history: tuple[ConversationMessageUnion, ...] | None = None
+
+
+class CommitInputs(IdentifierInputs):
+ # pyre-ignore[56]: pyre's stubs don't match pydantic v2 decorator signatures
+ @model_validator(mode="after")
+ def validate_goal_not_none(self) -> Self:
+ if self.maybe_goal is None:
+ raise IdentifierInputsMissingError("goal cannot be None for CommitInputs")
+ return self
+
+ # pyre-ignore[56]: pyre's stubs don't match pydantic v2 decorator signatures
+ @model_validator(mode="after")
+ def validate_diff_not_none(self) -> Self:
+ if self.maybe_diff is None:
+ raise IdentifierInputsMissingError("goal cannot be None for CommitInputs")
+ return self
+
+ @property
+ def goal(self) -> str:
+ assert self.maybe_goal is not None
+ return self.maybe_goal
+
+ @property
+ def diff(self) -> str:
+ assert self.maybe_diff is not None
+ return self.maybe_diff
+
+
+class ConversationInputs(IdentifierInputs):
+ # pyre-ignore[56]: pyre's stubs don't match pydantic v2 decorator signatures
+ @model_validator(mode="after")
+ def validate_conversation_history_not_none(self) -> Self:
+ if self.maybe_conversation_history is None:
+ raise IdentifierInputsMissingError("conversation_history is required for conversation inputs")
+ return self
+
+ @property
+ def conversation_history(self) -> tuple[ConversationMessageUnion, ...]:
+ assert self.maybe_conversation_history is not None
+ return self.maybe_conversation_history
+
+
+SpecificIdentifierInputsType = TypeVar("SpecificIdentifierInputsType", bound=IdentifierInputs)
+
+
+def to_specific_inputs_type(
+ identifier_inputs: IdentifierInputs, to_type: type[SpecificIdentifierInputsType]
+) -> SpecificIdentifierInputsType:
+ return to_type(**identifier_inputs.model_dump())
diff --git a/vet/imbue_tools/llm_output_parsing/parse_model_json_response.py b/vet/imbue_tools/llm_output_parsing/parse_model_json_response.py
@@ -0,0 +1,51 @@
+import json
+import re
+from typing import TypeVar
+
+from pydantic import ValidationError
+
+from vet.imbue_core.async_monkey_patches import log_exception
+from vet.imbue_core.pydantic_serialization import SerializableModel
+
+
+def parse_json_block_from_response_text(response_text: str) -> str:
+ """Clean markdown formatting and extra content from LLM response."""
+ response_text = response_text.strip()
+ # Parse content between first ```json and ``` block
+ json_start_line = re.search(r"^.*?```json\s*", response_text, flags=re.MULTILINE)
+ if json_start_line:
+ response_text = response_text[json_start_line.end() :]
+ json_end_line = re.search(r"```\s*$", response_text, flags=re.MULTILINE)
+ if json_end_line:
+ response_text = response_text[: json_end_line.start()]
+ return response_text.strip()
+
+
+ResponseSchema = TypeVar("ResponseSchema", bound=SerializableModel)
+
+
+class ResponseParsingError(Exception):
+ pass
+
+
+def parse_model_json_response(response_text: str, result_type: type[ResponseSchema]) -> ResponseSchema:
+ """Parse a JSON response from the LLM into a Pydantic model."""
+ cleaned_response = parse_json_block_from_response_text(response_text)
+ try:
+ return result_type.model_validate_json(cleaned_response)
+ except json.JSONDecodeError as e:
+ log_exception(
+ e,
+ "Response is not valid JSON.\nraw_response: {response_text}\ncleaned_response: {cleaned_response}",
+ response_text=response_text,
+ cleaned_response=cleaned_response,
+ )
+ raise ResponseParsingError(str(e)) from e
+ except ValidationError as e:
+ log_exception(
+ e,
+ "Response does not match the expected schema.\nraw_response: {response_text}\ncleaned_response: {cleaned_response}",
+ response_text=response_text,
+ cleaned_response=cleaned_response,
+ )
+ raise ResponseParsingError(str(e)) from e
diff --git a/imbue_tools/imbue_tools/py.typed b/vet/imbue_tools/py.typed
diff --git a/imbue_tools/imbue_tools/repo_utils/__init__.py b/vet/imbue_tools/repo_utils/__init__.py
diff --git a/vet/imbue_tools/repo_utils/context_prefix.py b/vet/imbue_tools/repo_utils/context_prefix.py
@@ -0,0 +1,613 @@
+import functools
+from enum import StrEnum
+from pathlib import Path
+from typing import Any
+from typing import Iterable
+from typing import Mapping
+from typing import assert_never
+
+from loguru import logger
+from pydantic import BaseModel
+from pydantic import ConfigDict
+
+from vet.imbue_core.agents.configs import LanguageModelGenerationConfig
+from vet.imbue_core.async_monkey_patches import log_exception
+from vet.imbue_core.pydantic_serialization import SerializableModel
+from vet.imbue_tools.repo_utils.context_utils import escape_prompt_markers
+from vet.imbue_tools.repo_utils.context_utils import maybe_get_file_path_from_qualified_name
+from vet.imbue_tools.repo_utils.data_types import FileContextUnion
+from vet.imbue_tools.repo_utils.errors import ContextLengthExceededError
+from vet.imbue_tools.repo_utils.file_system import InMemoryFileSystem
+from vet.imbue_tools.repo_utils.python_imports import QualifiedName
+from vet.imbue_tools.repo_utils.python_imports import STANDARD_LIBRARIES
+from vet.imbue_tools.repo_utils.python_imports import get_global_imports
+from vet.imbue_tools.repo_utils.subrepo_formatting import BaseFilenamePattern
+from vet.imbue_tools.repo_utils.subrepo_formatting import ContextFormatStyle
+from vet.imbue_tools.repo_utils.subrepo_formatting import ExactFilenamePattern
+from vet.imbue_tools.repo_utils.subrepo_formatting import FilenamePattern
+from vet.imbue_tools.repo_utils.subrepo_formatting import IntersectionFilenamePattern
+from vet.imbue_tools.repo_utils.subrepo_formatting import NegatedFilenamePattern
+from vet.imbue_tools.repo_utils.subrepo_formatting import REPO_CONTEXT_TEMPLATE
+from vet.imbue_tools.repo_utils.subrepo_formatting import SubrepoContextMatchers
+from vet.imbue_tools.repo_utils.subrepo_formatting import UnionFilenamePattern
+from vet.imbue_tools.repo_utils.subrepo_formatting import compute_file_context_format_styles
+from vet.imbue_tools.repo_utils.subrepo_formatting import format_subrepo_context
+from vet.imbue_tools.repo_utils.subrepo_formatting import (
+ parse_subrepo_context_matchers_from_toml,
+)
+
+
+class SubrepoContext(SerializableModel):
+ repo_context_files: tuple[FileContextUnion, ...]
+ subrepo_context_strategy_label: str
+
+
+class SubrepoContextWithFormattedContext(SubrepoContext):
+ formatted_repo_context: str
+
+
+def is_qualified_name_from_stdlib(qualified_name: QualifiedName) -> bool:
+ return qualified_name.top_level_name.value in STANDARD_LIBRARIES
+
+
+def get_immediate_first_party_import_paths_for_python_file(
+ current_file_path: str, full_repo_contents_map: InMemoryFileSystem
+) -> set[str] | None:
+ file_contents = full_repo_contents_map.get_text(current_file_path)
+ if not file_contents or not current_file_path.endswith(".py"):
+ return None
+
+ try:
+ global_imports = get_global_imports(file_contents)
+ except SyntaxError as e:
+ log_exception(
+ e,
+ "Failed to parse imports for {current_file_path}",
+ current_file_path=current_file_path,
+ )
+ return None
+
+ parent_names: set[QualifiedName] = set()
+ for import_ in global_imports:
+ parent_name = import_.qualified_name.parent_name
+ parent_names.add(parent_name)
+
+ imported_file_paths = set()
+ all_file_paths = [Path(x) for x in full_repo_contents_map.text_files.keys()]
+ for parent_name in parent_names:
+ other_file_path = maybe_get_file_path_from_qualified_name(parent_name, all_file_paths)
+ # if this doesn't exist it's likely not a first party import so we can ignore it
+ if not other_file_path or is_qualified_name_from_stdlib(parent_name):
+ continue
+ imported_file_paths.add(str(other_file_path))
+
+ return imported_file_paths
+
+
+FULL_REPO_PATHSPEC = BaseFilenamePattern.from_lines(["/**"])
+DOC_FILE_EXTENSIONS = [".md", ".txt"]
+DOC_PATHSPEC = BaseFilenamePattern.from_lines([f"**/*{ext}" for ext in DOC_FILE_EXTENSIONS])
+
+# Common files that we want to exclude since they can be large and are of low signal for issue identification.
+EXCLUSIONS_PATHSPEC = BaseFilenamePattern.from_lines(["uv.lock", "**/__snapshots__/**"])
+
+
+def escape_gitignore_pattern(path: str) -> str:
+ """
+ Escape a path into a GitIgnore pattern that matches exactly the path.
+
+ GitWildMatchPattern assigns special meaning to the following characters, which need to be escaped:
+ `*`, `?`, `[`, `]` and `\\`.
+ At the beginning of a line, we additionally need to escape leading `#` and `!` characters,
+ and at the end of a line we need to escape trailing ` ` (space) characters.
+ For simplicity, we simply escape these characters everywhere, which should still work correctly.
+ """
+ return (
+ path.replace("\\", "\\\\")
+ .replace("*", "\\*")
+ .replace("?", "\\?")
+ .replace("[", "\\[")
+ .replace("]", "\\]")
+ .replace("#", "\\#")
+ .replace("!", "\\!")
+ .replace(" ", "\\ ")
+ )
+
+
+def first_level_files_along_paths(file_paths: Iterable[str]) -> FilenamePattern:
+ """
+ Create a pathspec that matches all files along the given paths, but doesn't match adjacent directories.
+ """
+ # for each level in the path, we create an IntersectionFilenamePattern
+ # which has one branch that matches everything starting with that path,
+ # and another branch which matches everything except subdirectories starting with that path
+ # then we OR these all together as a UnionFilenamePattern
+ sorted_file_paths = sorted(file_paths)
+ file_patterns = []
+ for file_path in sorted_file_paths:
+ for parent in Path(file_path).parents:
+ escaped_parent = Path(escape_gitignore_pattern(str(parent)))
+ match_all = BaseFilenamePattern.from_lines([str("/" / escaped_parent / "*")])
+ match_except_subdirectories = NegatedFilenamePattern.build_from_positive_pattern(
+ BaseFilenamePattern.from_lines([str("/" / escaped_parent / "*/*")])
+ )
+ file_patterns.append(IntersectionFilenamePattern(specs=(match_all, match_except_subdirectories)))
+ return UnionFilenamePattern(specs=tuple(file_patterns))
+
+
+# cache this since it's reused across strategies
+@functools.lru_cache(maxsize=5)
+def make_docs_pathspec_along_paths(file_paths: frozenset[str]) -> FilenamePattern:
+ """
+ Create a pathspec that matches documentation files (.md, .txt) along each parent folder of the given file paths.
+ """
+ return IntersectionFilenamePattern(specs=(DOC_PATHSPEC, first_level_files_along_paths(file_paths=file_paths)))
+
+
+INSTRUCTIONS_PATHSPEC = BaseFilenamePattern.from_lines(["**/.claude.md", "**/CLAUDE.md", "**/AGENTS.md"])
+
+
+@functools.lru_cache(maxsize=5)
+def make_relevant_files_pathspec(file_paths: frozenset[str]) -> FilenamePattern:
+ """
+ Create a pathspec that matches the given file paths.
+ """
+ return ExactFilenamePattern(filenames=tuple(sorted(file_paths)))
+
+
+# cache this since it's reused across strategies
+@functools.lru_cache(maxsize=5)
+def make_instructions_pathspec_along_paths(
+ file_paths: frozenset[str],
+) -> FilenamePattern:
+ """
+ Create a pathspec that matches instruction files (e.g. .claude.md, CLAUDE.md, AGENTS.md) along each parent folder of the given file paths.
+
+ Should match a strict subset of make_docs_pathspec_along_paths.
+ """
+ return IntersectionFilenamePattern(
+ specs=(
+ INSTRUCTIONS_PATHSPEC,
+ first_level_files_along_paths(file_paths=file_paths),
+ )
+ )
+
+
+# cache this since it's reused across strategies
+@functools.lru_cache(maxsize=5)
+def make_imports_pathspec_for_paths(
+ file_paths: frozenset[str], full_repo_contents: InMemoryFileSystem
+) -> FilenamePattern:
+ """
+ Create a pathspec that matches Python files that are imported by the given file paths.
+ """
+ full_repo_python_file_contents_map = InMemoryFileSystem.build(
+ {k: v for k, v in full_repo_contents.files.items() if k.endswith(".py")}
+ )
+
+ imported_file_paths = set()
+ for file_path in file_paths:
+ if file_path.endswith(".py"):
+ # Include first party imports
+ imported_paths = get_immediate_first_party_import_paths_for_python_file(
+ file_path, full_repo_python_file_contents_map
+ )
+ if imported_paths:
+ imported_file_paths.update(imported_paths)
+
+ return ExactFilenamePattern(filenames=tuple(sorted(imported_file_paths)))
+
+
+class SubrepoContextStrategy(BaseModel):
+ model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)
+ label: str
+ matchers: SubrepoContextMatchers
+
+
+class SubrepoContextStrategyType(StrEnum):
+ # defaults if we have relevant files
+ FULL_REPO_CONTENTS = "full repo contents"
+ RELEVANT_WHOLE_FILES_IMPORTS_DOCS_AND_ELSEWHERE_FILENAME = (
+ "relevant files + immediate imports + docs along relevant paths + filenames elsewhere"
+ )
+ RELEVANT_WHOLE_FILES_IMPORTS_DOCS = "relevant files + immediate imports + docs along relevant paths"
+ RELEVANT_WHOLE_FILES_AND_RELEVANT_STUBBIFIED_IMPORTS_DOCS = (
+ "relevant files + stubbified imports + docs along relevant paths"
+ )
+ RELEVANT_WHOLE_FILES_DOCS = "relevant files + docs along relevant paths"
+ RELEVANT_WHOLE_FILES_INSTRUCTIONS = "relevant files + agent instructions along relevant paths"
+ RELEVANT_WHOLE_FILES = "relevant files"
+ RELEVANT_STUBBIFIED_FILES = "relevant stubbified files"
+ NOTHING = "nothing"
+
+ # defaults if we don't have relevant files (missing FULL_REPO_CONTENTS and NOTHING because they're already listed for if we do have relevant files)
+ WHOLE_DOCS_AND_OTHERWISE_FILENAMES = "docs + filenames elsewhere"
+ WHOLE_INSTRUCTIONS_AND_OTHERWISE_FILENAMES = "agent instructions + filenames elsewhere"
+ WHOLE_INSTRUCTIONS = "agent instructions"
+
+ # defaults for providing instruction files if we have relevant files
+ WHOLE_DOCS = "docs"
+ WHOLE_INSTRUCTIONS_AND_RELEVANT_DOCS = "agent instructions + relevant docs"
+ RELEVANT_DOCS = "relevant docs"
+ RELEVANT_INSTRUCTIONS = "relevant agent instructions"
+
+ # custom
+ CUSTOM = "custom"
+
+
+class StrategyMode(StrEnum):
+ REGULAR = "regular"
+ DOCS = "docs"
+
+
+class AvailableInfoMode(StrEnum):
+ YES_FILES = "yes_files"
+ NO_FILES = "no_files"
+
+
+DEFAULT_STRATEGY_TYPES: dict[tuple[StrategyMode, AvailableInfoMode], tuple[SubrepoContextStrategyType, ...]] = {
+ (StrategyMode.REGULAR, AvailableInfoMode.YES_FILES): (
+ SubrepoContextStrategyType.FULL_REPO_CONTENTS,
+ SubrepoContextStrategyType.RELEVANT_WHOLE_FILES_IMPORTS_DOCS_AND_ELSEWHERE_FILENAME,
+ SubrepoContextStrategyType.RELEVANT_WHOLE_FILES_IMPORTS_DOCS,
+ SubrepoContextStrategyType.RELEVANT_WHOLE_FILES_AND_RELEVANT_STUBBIFIED_IMPORTS_DOCS,
+ SubrepoContextStrategyType.RELEVANT_WHOLE_FILES_DOCS,
+ SubrepoContextStrategyType.RELEVANT_WHOLE_FILES_INSTRUCTIONS,
+ SubrepoContextStrategyType.RELEVANT_WHOLE_FILES,
+ SubrepoContextStrategyType.RELEVANT_STUBBIFIED_FILES,
+ SubrepoContextStrategyType.NOTHING,
+ ),
+ (StrategyMode.REGULAR, AvailableInfoMode.NO_FILES): (
+ SubrepoContextStrategyType.FULL_REPO_CONTENTS,
+ SubrepoContextStrategyType.WHOLE_DOCS_AND_OTHERWISE_FILENAMES,
+ SubrepoContextStrategyType.WHOLE_INSTRUCTIONS_AND_OTHERWISE_FILENAMES,
+ SubrepoContextStrategyType.WHOLE_INSTRUCTIONS,
+ SubrepoContextStrategyType.NOTHING,
+ ),
+ (StrategyMode.DOCS, AvailableInfoMode.YES_FILES): (
+ SubrepoContextStrategyType.WHOLE_DOCS,
+ SubrepoContextStrategyType.WHOLE_INSTRUCTIONS_AND_RELEVANT_DOCS,
+ SubrepoContextStrategyType.RELEVANT_DOCS,
+ SubrepoContextStrategyType.RELEVANT_INSTRUCTIONS,
+ # these don't have `nothing` strategies because having some user instructions is crucial for
+ # the issue identifier which uses this, whereas the others can do ok with just a diff
+ ),
+ (StrategyMode.DOCS, AvailableInfoMode.NO_FILES): (
+ SubrepoContextStrategyType.WHOLE_DOCS,
+ SubrepoContextStrategyType.WHOLE_INSTRUCTIONS,
+ ),
+}
+
+
+def build_strategy(
+ strategy_type: SubrepoContextStrategyType,
+ full_repo_contents: InMemoryFileSystem,
+ relevant_file_paths: frozenset[str] | None,
+) -> SubrepoContextStrategy:
+ match strategy_type:
+ case SubrepoContextStrategyType.FULL_REPO_CONTENTS as s:
+ return SubrepoContextStrategy(
+ label=s,
+ matchers=((ContextFormatStyle.FULL_FILE, FULL_REPO_PATHSPEC),),
+ )
+ case SubrepoContextStrategyType.RELEVANT_WHOLE_FILES_IMPORTS_DOCS_AND_ELSEWHERE_FILENAME as s:
+ return SubrepoContextStrategy(
+ label=s,
+ matchers=(
+ (
+ ContextFormatStyle.FULL_FILE,
+ make_relevant_files_pathspec(relevant_file_paths),
+ ),
+ (
+ ContextFormatStyle.FULL_FILE,
+ make_imports_pathspec_for_paths(relevant_file_paths, full_repo_contents),
+ ),
+ (
+ ContextFormatStyle.FULL_FILE,
+ make_docs_pathspec_along_paths(relevant_file_paths),
+ ),
+ (ContextFormatStyle.FILENAME_ONLY, FULL_REPO_PATHSPEC),
+ ),
+ )
+ case SubrepoContextStrategyType.RELEVANT_WHOLE_FILES_IMPORTS_DOCS as s:
+ return SubrepoContextStrategy(
+ label=s,
+ matchers=(
+ (
+ ContextFormatStyle.FULL_FILE,
+ make_relevant_files_pathspec(relevant_file_paths),
+ ),
+ (
+ ContextFormatStyle.FULL_FILE,
+ make_imports_pathspec_for_paths(relevant_file_paths, full_repo_contents),
+ ),
+ (
+ ContextFormatStyle.FULL_FILE,
+ make_docs_pathspec_along_paths(relevant_file_paths),
+ ),
+ (ContextFormatStyle.HIDDEN, FULL_REPO_PATHSPEC),
+ ),
+ )
+ case SubrepoContextStrategyType.RELEVANT_WHOLE_FILES_AND_RELEVANT_STUBBIFIED_IMPORTS_DOCS as s:
+ return SubrepoContextStrategy(
+ label=s,
+ matchers=(
+ (
+ ContextFormatStyle.FULL_FILE,
+ make_relevant_files_pathspec(relevant_file_paths),
+ ),
+ (
+ ContextFormatStyle.STUB,
+ make_imports_pathspec_for_paths(relevant_file_paths, full_repo_contents),
+ ),
+ (
+ ContextFormatStyle.FULL_FILE,
+ make_docs_pathspec_along_paths(relevant_file_paths),
+ ),
+ (ContextFormatStyle.HIDDEN, FULL_REPO_PATHSPEC),
+ ),
+ )
+ case SubrepoContextStrategyType.RELEVANT_WHOLE_FILES_DOCS as s:
+ return SubrepoContextStrategy(
+ label=s,
+ matchers=(
+ (
+ ContextFormatStyle.FULL_FILE,
+ make_relevant_files_pathspec(relevant_file_paths),
+ ),
+ (
+ ContextFormatStyle.FULL_FILE,
+ make_docs_pathspec_along_paths(relevant_file_paths),
+ ),
+ (ContextFormatStyle.HIDDEN, FULL_REPO_PATHSPEC),
+ ),
+ )
+ case SubrepoContextStrategyType.RELEVANT_WHOLE_FILES_INSTRUCTIONS as s:
+ return SubrepoContextStrategy(
+ label=s,
+ matchers=(
+ (
+ ContextFormatStyle.FULL_FILE,
+ make_relevant_files_pathspec(relevant_file_paths),
+ ),
+ (
+ ContextFormatStyle.FULL_FILE,
+ make_instructions_pathspec_along_paths(relevant_file_paths),
+ ),
+ (ContextFormatStyle.HIDDEN, FULL_REPO_PATHSPEC),
+ ),
+ )
+ case SubrepoContextStrategyType.RELEVANT_WHOLE_FILES as s:
+ return SubrepoContextStrategy(
+ label=s,
+ matchers=(
+ (
+ ContextFormatStyle.FULL_FILE,
+ make_relevant_files_pathspec(relevant_file_paths),
+ ),
+ (ContextFormatStyle.HIDDEN, FULL_REPO_PATHSPEC),
+ ),
+ )
+ case SubrepoContextStrategyType.RELEVANT_STUBBIFIED_FILES as s:
+ return SubrepoContextStrategy(
+ label=s,
+ matchers=(
+ (
+ ContextFormatStyle.STUB,
+ make_relevant_files_pathspec(relevant_file_paths),
+ ),
+ (ContextFormatStyle.HIDDEN, FULL_REPO_PATHSPEC),
+ ),
+ )
+ case SubrepoContextStrategyType.NOTHING as s:
+ return SubrepoContextStrategy(
+ label=s,
+ matchers=((ContextFormatStyle.HIDDEN, FULL_REPO_PATHSPEC),),
+ )
+
+ case SubrepoContextStrategyType.WHOLE_DOCS_AND_OTHERWISE_FILENAMES as s:
+ return SubrepoContextStrategy(
+ label=s,
+ matchers=(
+ (ContextFormatStyle.FULL_FILE, DOC_PATHSPEC),
+ (ContextFormatStyle.FILENAME_ONLY, FULL_REPO_PATHSPEC),
+ ),
+ )
+ case SubrepoContextStrategyType.WHOLE_INSTRUCTIONS_AND_OTHERWISE_FILENAMES as s:
+ return SubrepoContextStrategy(
+ label=s,
+ matchers=(
+ (ContextFormatStyle.FULL_FILE, INSTRUCTIONS_PATHSPEC),
+ (ContextFormatStyle.FILENAME_ONLY, FULL_REPO_PATHSPEC),
+ ),
+ )
+ case SubrepoContextStrategyType.WHOLE_INSTRUCTIONS as s:
+ return SubrepoContextStrategy(
+ label=s,
+ matchers=(
+ (ContextFormatStyle.FULL_FILE, INSTRUCTIONS_PATHSPEC),
+ (ContextFormatStyle.HIDDEN, FULL_REPO_PATHSPEC),
+ ),
+ )
+
+ case SubrepoContextStrategyType.WHOLE_DOCS as s:
+ return SubrepoContextStrategy(
+ label=s,
+ matchers=(
+ (ContextFormatStyle.FULL_FILE, DOC_PATHSPEC),
+ (ContextFormatStyle.HIDDEN, FULL_REPO_PATHSPEC),
+ ),
+ )
+ case SubrepoContextStrategyType.WHOLE_INSTRUCTIONS_AND_RELEVANT_DOCS as s:
+ return SubrepoContextStrategy(
+ label=s,
+ matchers=(
+ (ContextFormatStyle.FULL_FILE, INSTRUCTIONS_PATHSPEC),
+ (
+ ContextFormatStyle.FULL_FILE,
+ make_docs_pathspec_along_paths(relevant_file_paths),
+ ),
+ (ContextFormatStyle.HIDDEN, FULL_REPO_PATHSPEC),
+ ),
+ )
+ case SubrepoContextStrategyType.RELEVANT_DOCS as s:
+ return SubrepoContextStrategy(
+ label=s,
+ matchers=(
+ (
+ ContextFormatStyle.FULL_FILE,
+ make_docs_pathspec_along_paths(relevant_file_paths),
+ ),
+ (ContextFormatStyle.HIDDEN, FULL_REPO_PATHSPEC),
+ ),
+ )
+ case SubrepoContextStrategyType.RELEVANT_INSTRUCTIONS as s:
+ return SubrepoContextStrategy(
+ label=s,
+ matchers=(
+ (
+ ContextFormatStyle.FULL_FILE,
+ make_instructions_pathspec_along_paths(relevant_file_paths),
+ ),
+ (ContextFormatStyle.HIDDEN, FULL_REPO_PATHSPEC),
+ ),
+ )
+ case _ as unreachable:
+ assert_never(unreachable) # pyre-ignore[6]: pyre doesn't understand enums
+
+
+def generate_subrepo_strategies(
+ mode: StrategyMode,
+ full_repo_contents: InMemoryFileSystem,
+ relevant_file_paths: frozenset[str] | None = None,
+) -> list[SubrepoContextStrategy]:
+ available_info = AvailableInfoMode.YES_FILES if relevant_file_paths else AvailableInfoMode.NO_FILES
+ return [
+ build_strategy(strategy_type, full_repo_contents, relevant_file_paths)
+ for strategy_type in DEFAULT_STRATEGY_TYPES[(mode, available_info)]
+ ]
+
+
+def select_desired_subrepo_strategies(
+ full_repo_contents: InMemoryFileSystem,
+ relevant_file_paths: frozenset[str] | None = None,
+ subrepo_context_config: str | None = None,
+ strategy_types_to_try: tuple[SubrepoContextStrategyType] | None = None,
+ strategy_mode: StrategyMode | None = None, # if no config option is set, defaults to StrategyMode.REGULAR
+) -> list[SubrepoContextStrategy]:
+ num_ways_config_was_set = sum(
+ 1 for v in [subrepo_context_config, strategy_types_to_try, strategy_mode] if v is not None
+ )
+ if num_ways_config_was_set > 1:
+ assert False, "Can only specify one of subrepo_context_config, strategy_types_to_try, and strategy_mode"
+
+ if subrepo_context_config is not None:
+ # An explicit subrepo context config was provided. Use it exclusively.
+ subrepo_context_matchers = parse_subrepo_context_matchers_from_toml(subrepo_context_config)
+ return [
+ SubrepoContextStrategy(
+ label=SubrepoContextStrategyType.CUSTOM,
+ matchers=subrepo_context_matchers,
+ )
+ ]
+ elif strategy_types_to_try is not None:
+ return [
+ build_strategy(strategy_type, full_repo_contents, relevant_file_paths)
+ for strategy_type in strategy_types_to_try
+ ]
+ else:
+ strategy_mode_to_use = strategy_mode if strategy_mode is not None else StrategyMode.REGULAR
+ return generate_subrepo_strategies(
+ strategy_mode_to_use,
+ full_repo_contents=full_repo_contents,
+ relevant_file_paths=relevant_file_paths,
+ )
+
+
+# Caching results because this function is quite expensive. We compose multiple repo_context prefixes, and
+# also have to tokenize them to check their respective lengths. Both of these operations are expensive,
+@functools.lru_cache(maxsize=10)
+def get_repo_context(
+ model_config: LanguageModelGenerationConfig,
+ full_repo_contents: InMemoryFileSystem,
+ # how many tokens to reserve for additional prompt messages and output
+ tokens_to_reserve: int,
+ relevant_file_paths: frozenset[str] | None = None,
+ subrepo_context_config: str | None = None,
+ strategy_types_to_try: tuple[SubrepoContextStrategyType] | None = None,
+ strategy_mode: StrategyMode | None = None, # if no config option is set, defaults to StrategyMode.REGULAR
+ template: str = REPO_CONTEXT_TEMPLATE,
+) -> SubrepoContextWithFormattedContext:
+ """
+ Make sure to try pass the same `full_repo_contents` when making multiple similar calls.
+ Ordering of the dict is relevant for caching.
+ """
+ subrepo_context_strategies_to_try = select_desired_subrepo_strategies(
+ full_repo_contents,
+ relevant_file_paths,
+ subrepo_context_config,
+ strategy_types_to_try,
+ strategy_mode,
+ )
+
+ last_context_length_exceeded_error: ContextLengthExceededError | None = None
+ for subrepo_context_strategy in subrepo_context_strategies_to_try:
+ try:
+ path_to_format_style = compute_file_context_format_styles(
+ file_paths=full_repo_contents.text_files.keys(),
+ subrepo_context_matchers=subrepo_context_strategy.matchers,
+ exclusions=EXCLUSIONS_PATHSPEC,
+ )
+ repo_context_str, repo_context_files = format_subrepo_context(
+ full_repo_contents=full_repo_contents.text_files,
+ model_config=model_config,
+ path_to_format_style=path_to_format_style,
+ tokens_to_reserve=tokens_to_reserve,
+ template=template,
+ )
+ logger.info("Selected subrepo context strategy: {}", subrepo_context_strategy.label)
+
+ if subrepo_context_strategy.label == SubrepoContextStrategyType.NOTHING:
+ # log an error if we have to use the NOTHING strategy, but still proceed with the call
+ logger.error("Selected NOTHING subrepo context strategy; hopefully this doesn't happen too often!")
+
+ return SubrepoContextWithFormattedContext(
+ formatted_repo_context=repo_context_str,
+ repo_context_files=repo_context_files,
+ subrepo_context_strategy_label=subrepo_context_strategy.label,
+ )
+ except ContextLengthExceededError as e:
+ last_context_length_exceeded_error = e
+
+ # We have exhausted all subrepo context strategies, and none of them worked.
+ assert last_context_length_exceeded_error is not None
+ raise last_context_length_exceeded_error from last_context_length_exceeded_error
+
+
+# TODO: why not just render this here?
+def create_context_prompt_prefix(repo_context: str) -> tuple[str, Mapping[str, Any]]:
+ """Create a message that provides context about the repo contents."""
+ cached_prefix_template = """[ROLE=SYSTEM_CACHED]
+You are a detail-oriented, expert software developer.
+
+Your goal is to help the user develop a particular commit to make a change to their program.
+
+{{repo_context}}
+
+{% if recent_git_history -%}
+
+As additional context, here are some of the most recent changes made to the codebase (the output of `git log` and diffs for each of those commits):
+
+```
+{{recent_git_history}}
+```
+{% endif -%}
+"""
+
+ return (
+ cached_prefix_template,
+ dict(
+ repo_context=escape_prompt_markers(repo_context),
+ recent_git_history=None,
+ ),
+ )
diff --git a/vet/imbue_tools/repo_utils/context_retrieval.py b/vet/imbue_tools/repo_utils/context_retrieval.py
@@ -0,0 +1,121 @@
+import threading
+import time
+from pathlib import Path
+from typing import Generator
+
+import pygit2
+from loguru import logger
+from pygit2.enums import ObjectType
+from pygit2.repository import Repository
+
+from vet.imbue_core.async_utils import make_async
+from vet.imbue_tools.repo_utils.diff_utils import apply_diffs_to_files
+from vet.imbue_tools.repo_utils.file_system import FileContents
+from vet.imbue_tools.repo_utils.file_system import InMemoryFileSystem
+from vet.imbue_tools.repo_utils.file_system import SymlinkContents
+
+
+class RepoContextManagerError(Exception):
+ pass
+
+
+class RepoContextManager:
+ """A manager for handling retrieval of files, etc from the repo."""
+
+ def __init__(self, repo_path: Path, project_name: str) -> None:
+ self.project_name = project_name
+ self.repo_path = repo_path
+ self._repo = Repository(path=str(repo_path))
+
+ # We need the sync lock due to pygit2 being synchronous.
+ # It is mostly used for the blob data cache, but also for the repo contents by git hash cache.
+ self._lock = threading.Lock()
+
+ @classmethod
+ def build(cls, repo_path: Path) -> "RepoContextManager":
+ try:
+ # make sure we are in a git repo
+ Repository(path=str(repo_path))
+ except pygit2.GitError as e:
+ raise RepoContextManagerError(f"Failed to initialize git repo at {repo_path}") from e
+
+ repo_context_manager = cls(repo_path=repo_path, project_name=repo_path.name)
+ return repo_context_manager
+
+ async def get_full_repo_contents_at_repo_state(self, git_hash: str, diff: str) -> InMemoryFileSystem:
+ final_contents = await self.get_full_repo_contents_at_commit(git_hash)
+ final_contents = await apply_diffs_to_files(final_contents, (diff,))
+ return final_contents
+
+ def get_full_repo_contents_at_commit_sync(self, git_hash: str) -> InMemoryFileSystem:
+ # NOTE: most of the time we want to get the contents at a repo state, not a git hash.
+ # Call get_full_repo_contents_at_repo_state instead in that case.
+ with self._lock:
+ start_time = time.perf_counter()
+
+ # Assert against use of HEAD specifically because there could be some existing code
+ # that uses it, and we want to catch that. It would fail below as well with a KeyError,
+ # but this assert makes the exception message more explicit.
+ assert git_hash != "HEAD", "Only proper commit hashes are supported, not HEAD"
+ commit = self._repo[git_hash]
+ assert isinstance(commit, pygit2.Commit), f"Expected a pygit2.Commit, got {type(commit)}"
+
+ full_repo_contents = self._read_blobs_from_commit(commit)
+
+ end_time = time.perf_counter()
+ logger.debug(
+ "Loaded full repo contents for git hash {git_hash} in {duration:.2f} seconds",
+ git_hash=git_hash,
+ duration=end_time - start_time,
+ )
+ return full_repo_contents
+
+ @make_async
+ def get_full_repo_contents_at_commit(self, git_hash: str) -> InMemoryFileSystem:
+ return self.get_full_repo_contents_at_commit_sync(git_hash)
+
+ def _read_blobs_from_commit(self, commit: pygit2.Commit) -> InMemoryFileSystem:
+ """Read all blobs in a given commit."""
+ file_system_dict: dict[str, FileContents] = {}
+
+ for path, blob in self._list_blobs_from_tree(commit.tree, skip_binary=False, skip_symlinks=False):
+ if blob.filemode == 0o120000:
+ # Blob is a symbolic link. Its contents in git represent the target path.
+ file_system_dict[path] = SymlinkContents(target_path=blob.data.decode("utf-8"))
+ else:
+ file_system_dict[path] = blob.data
+ return InMemoryFileSystem.build(file_system_dict)
+
+ def _list_blobs_from_tree(
+ self, tree: pygit2.Tree, skip_binary: bool, skip_symlinks: bool
+ ) -> Generator[tuple[str, pygit2.Blob], None, None]:
+ """Recursively list all blobs in a tree, including its subtrees."""
+ assert self._lock.locked()
+ for entry in tree:
+ if entry.type == ObjectType.BLOB:
+ assert isinstance(entry, pygit2.Blob)
+ if skip_binary and entry.is_binary:
+ continue
+ if skip_symlinks and entry.filemode == 0o120000:
+ continue
+
+ blob_path = entry.name
+ assert blob_path is not None
+ yield blob_path, entry
+
+ elif entry.type == ObjectType.TREE:
+ assert isinstance(entry, pygit2.Tree)
+ # Recurse into a subtree (folder)
+ sub_tree = self._repo[entry.id]
+ assert isinstance(sub_tree, pygit2.Tree)
+ for sub_path, sub_blob in self._list_blobs_from_tree(
+ sub_tree, skip_binary=skip_binary, skip_symlinks=skip_symlinks
+ ):
+ yield f"{entry.name}/{sub_path}", sub_blob
+
+ elif entry.type == ObjectType.COMMIT:
+ # A COMMIT object indicates a submodule, which we do not traverse for the time being.
+ logger.info("Skipping submodule in repo context: {}", entry.name)
+
+ else:
+ raise ValueError(f"Unexpected entry type in git tree: {entry.type}")
diff --git a/vet/imbue_tools/repo_utils/context_utils.py b/vet/imbue_tools/repo_utils/context_utils.py
@@ -0,0 +1,52 @@
+from pathlib import Path
+from typing import Iterable
+
+from vet.imbue_tools.repo_utils.python_imports import QualifiedName
+
+
+def escape_prompt_markers(text: str) -> str:
+ markers = [
+ "[ROLE=ASSISTANT]",
+ "[ROLE=USER]",
+ "[ROLE=USER_CACHED]",
+ "[ROLE=SYSTEM]",
+ "[ROLE=SYSTEM_CACHED]",
+ "[ROLE=HUMAN]",
+ ]
+ for marker in markers:
+ text = text.replace(marker, f"[{marker}]")
+ return text
+
+
+def escape_all_jinja_variables(text: str) -> str:
+ return "{% raw %}" + text + "{% endraw %}"
+
+
+def does_relative_path_match_target_path_suffix(target_path: Path, relative_file_path: Path) -> bool:
+ """
+ Checks if the parts of a relative path match the suffix of a target path.
+ """
+ possible_parts = relative_file_path.parts
+ target_parts = target_path.parts
+
+ if len(possible_parts) > len(target_parts):
+ return False
+
+ for i in range(1, len(possible_parts) + 1):
+ if possible_parts[-i] != target_parts[-i]:
+ return False
+ return True
+
+
+def maybe_get_file_path_from_qualified_name(
+ qualified_name: QualifiedName, all_file_paths: Iterable[Path]
+) -> Path | None:
+ """
+ Tries to find the file path that corresponds to qualified name. This requires the qualified name to be a file in the repo.
+ """
+ possible_relative_file_path = qualified_name.to_path()
+ # NOTE: it's possible to make this faster by doing some upfront computation
+ for target_file_path in all_file_paths:
+ if does_relative_path_match_target_path_suffix(target_file_path, possible_relative_file_path):
+ return target_file_path
+ return None
diff --git a/vet/imbue_tools/repo_utils/data_types.py b/vet/imbue_tools/repo_utils/data_types.py
@@ -0,0 +1,51 @@
+from abc import ABC
+from abc import abstractmethod
+from typing import Annotated
+
+from pydantic import Tag
+
+from vet.imbue_core.pydantic_serialization import SerializableModel
+from vet.imbue_core.pydantic_serialization import build_discriminator
+
+
+class FileContext(ABC, SerializableModel):
+ object_type: str
+ path: str
+
+ @abstractmethod
+ def format_for_agent(self) -> str:
+ pass
+
+
+class FullFileContext(FileContext):
+ object_type: str = "FullFileContext"
+ path: str
+ contents: str = "RAW FILE CONTENTS"
+
+ def format_for_agent(self) -> str:
+ return f"<FILE>\n<PATH>\n{self.path}\n</PATH>\n<CONTENTS>\n{self.contents}\n</CONTENTS>\n</FILE>\n\n"
+
+
+class FilenameContext(FileContext):
+ object_type: str = "FilenameContext"
+ path: str
+
+ def format_for_agent(self) -> str:
+ return f"<FILE>\n<PATH>\n{self.path}\n</PATH>\n</FILE>\n\n"
+
+
+class StubFileContext(FileContext):
+ object_type: str = "StubFileContext"
+ path: str
+ stub: str
+
+ def format_for_agent(self) -> str:
+ return f"<FILE>\n<PATH>\n{self.path}\n</PATH>\n<STUBIFIED_CONTENTS>\n{self.stub}\n</STUBIFIED_CONTENTS>\n</FILE>\n\n"
+
+
+FileContextUnion = Annotated[
+ Annotated[FullFileContext, Tag("FullFileContext")]
+ | Annotated[FilenameContext, Tag("FilenameContext")]
+ | Annotated[StubFileContext, Tag("StubFileContext")],
+ build_discriminator(),
+]
diff --git a/vet/imbue_tools/repo_utils/diff_utils.py b/vet/imbue_tools/repo_utils/diff_utils.py
@@ -0,0 +1,96 @@
+import re
+import subprocess
+import tempfile
+from pathlib import Path
+
+import pygit2
+from async_lru import alru_cache # type: ignore[undefined-attribute]: pyre on modal has an issue with this
+from loguru import logger
+
+from vet.imbue_tools.repo_utils.errors import DiffApplicationError
+from vet.imbue_tools.repo_utils.file_system import FileContents
+from vet.imbue_tools.repo_utils.file_system import InMemoryFileSystem
+from vet.imbue_tools.repo_utils.file_system import SymlinkContents
+from vet.imbue_tools.repo_utils.file_system_utils import (
+ create_initial_placeholder_commit_for_dir,
+)
+from vet.imbue_tools.repo_utils.file_system_utils import (
+ temporary_local_dir_from_in_memory_file_system,
+)
+
+
+@alru_cache
+async def apply_diffs_to_files(file_contents: InMemoryFileSystem, diff_strings: tuple[str, ...]) -> InMemoryFileSystem:
+ # Have to do this wrapping and unwrapping into dicts to allow @alru_cache to work
+ files_with_diffs = file_contents
+ for diff_string in diff_strings:
+ files_with_diffs = await _apply_diff_to_files(file_contents=files_with_diffs, diff_string=diff_string)
+ return files_with_diffs
+
+
+async def _apply_diff_to_files(file_contents: InMemoryFileSystem, diff_string: str) -> InMemoryFileSystem:
+ if diff_string.strip() == "":
+ return file_contents
+
+ file_pattern = re.compile(r"^diff --git a/(.+?) b/(.+)$", re.MULTILINE)
+ matches = file_pattern.findall(diff_string)
+
+ relevant_file_contents_dict = {}
+ for match in matches:
+ assert len(match) == 2
+ for file_path in match:
+ contents = file_contents.get(file_path, None)
+ if contents is not None:
+ relevant_file_contents_dict[file_path] = contents
+
+ async with temporary_local_dir_from_in_memory_file_system(
+ InMemoryFileSystem.build(relevant_file_contents_dict)
+ ) as temp_repo_dir:
+ repo = pygit2.init_repository(temp_repo_dir, bare=False)
+ create_initial_placeholder_commit_for_dir(repo)
+
+ with tempfile.NamedTemporaryFile(delete=False) as temp_patch_file:
+ temp_patch_file.write(diff_string.encode("utf-8"))
+ temp_patch_file.flush()
+ patch_file_path = temp_patch_file.name
+
+ try:
+ result = subprocess.run(
+ ("git", "apply", "--verbose", patch_file_path),
+ cwd=temp_repo_dir,
+ capture_output=True,
+ text=True,
+ timeout=10.0,
+ check=True,
+ )
+ except Exception as e:
+ logger.trace("Unable to apply patch: {error}", error=e)
+ raise DiffApplicationError from e
+
+ try:
+ updated_file_contents = _read_file_contents_from_dir_without_git(temp_repo_dir)
+ except Exception as e:
+ raise DiffApplicationError from e
+
+ combined_file_contents_dict = dict(updated_file_contents.files)
+ for file_path, contents in file_contents.files.items():
+ if file_path not in relevant_file_contents_dict:
+ combined_file_contents_dict[file_path] = contents
+
+ return InMemoryFileSystem.build(combined_file_contents_dict)
+
+
+def _read_file_contents_from_dir_without_git(dir_path_str: str) -> InMemoryFileSystem:
+ file_system_dict: dict[str, FileContents] = {}
+ for file_path in Path(dir_path_str).rglob("*"):
+ if ".git" in file_path.parts:
+ continue
+ if file_path.is_symlink():
+ relative_path = str(file_path.relative_to(dir_path_str))
+ target_path = str(file_path.readlink())
+ file_system_dict[relative_path] = SymlinkContents(target_path=target_path)
+ elif file_path.is_file():
+ relative_path = str(file_path.relative_to(dir_path_str))
+ with open(file_path, "rb") as file:
+ file_system_dict[relative_path] = file.read()
+ return InMemoryFileSystem.build(file_system_dict)
diff --git a/imbue_tools/imbue_tools/repo_utils/errors.py b/vet/imbue_tools/repo_utils/errors.py
diff --git a/vet/imbue_tools/repo_utils/file_system.py b/vet/imbue_tools/repo_utils/file_system.py
@@ -0,0 +1,71 @@
+from typing import Generator
+from typing import Mapping
+
+from vet.imbue_core.frozen_utils import FrozenDict
+from vet.imbue_core.frozen_utils import deep_freeze_mapping
+from vet.imbue_core.pydantic_serialization import SerializableModel
+
+
+class SymlinkContents(SerializableModel):
+ """A special type to represent a symbolic link in a file system."""
+
+ target_path: str
+
+ # Need to make SymlinkContents non-Iterable, or else deep_freeze_mapping will convert this to a tuple in InMemoryFileSystem.build.
+ __iter__ = None # type: ignore
+
+
+DecodedTextFileContents = str
+FileContents = bytes | SymlinkContents
+
+
+class InMemoryFileSystem(SerializableModel):
+ """
+ A simple representation of in-memory file system. Can contain both text and binary files.
+ """
+
+ # Mapping from file path to contents
+ files: FrozenDict[str, FileContents]
+ # Only text files, decoded as UTF-8. Excludes symlinks and binary files.
+ text_files: FrozenDict[str, DecodedTextFileContents]
+
+ @classmethod
+ def build(cls, files: Mapping[str, FileContents]) -> "InMemoryFileSystem":
+ sorted_files = {k: v for k, v in sorted(files.items())}
+ sorted_decoded_files: dict[str, DecodedTextFileContents | None] = {
+ k: _try_decode_file_contents(c) for k, c in sorted_files.items()
+ }
+ sorted_text_files: dict[str, DecodedTextFileContents] = {
+ k: c for k, c in sorted_decoded_files.items() if c is not None
+ }
+ return cls(
+ files=deep_freeze_mapping(sorted_files),
+ text_files=deep_freeze_mapping(sorted_text_files),
+ )
+
+ def get(self, file_path: str, default: FileContents | None = None) -> FileContents | None:
+ if file_path in self.files:
+ return self.files[file_path]
+ return default
+
+ def get_text(
+ self, file_path: str, default: DecodedTextFileContents | None = None
+ ) -> DecodedTextFileContents | None:
+ """Get a the contents of a text file as a string. Returns `default` if the file does not exist, is a symlink, or is a binary file."""
+ if file_path in self.text_files:
+ return self.text_files[file_path]
+ return default
+
+ def __iter__(self) -> Generator[tuple[str, FileContents], None, None]:
+ return (file for file in self.files.items())
+
+
+def _try_decode_file_contents(contents: FileContents) -> DecodedTextFileContents | None:
+ if isinstance(contents, SymlinkContents):
+ return None
+ else:
+ assert isinstance(contents, bytes)
+ try:
+ return contents.decode("utf-8")
+ except UnicodeDecodeError:
+ return None
diff --git a/vet/imbue_tools/repo_utils/file_system_utils.py b/vet/imbue_tools/repo_utils/file_system_utils.py
@@ -0,0 +1,70 @@
+import asyncio
+import tempfile
+from contextlib import asynccontextmanager
+from pathlib import Path
+from typing import AsyncGenerator
+from typing import cast
+
+import anyio
+import pygit2
+from loguru import logger
+
+from vet.imbue_tools.repo_utils.file_system import FileContents
+from vet.imbue_tools.repo_utils.file_system import InMemoryFileSystem
+from vet.imbue_tools.repo_utils.file_system import SymlinkContents
+
+
+async def write_file_contents_to_dir(file_contents: InMemoryFileSystem, dir_path_str: str) -> None:
+ dir_path = Path(dir_path_str)
+ tasks = [
+ asyncio.create_task(_write_single_file_to_dir(dir_path / file_path, content))
+ for file_path, content in file_contents.files.items()
+ ]
+ await asyncio.gather(*tasks)
+
+
+async def _write_single_file_to_dir(full_path: Path, content: FileContents) -> None:
+ await anyio.to_thread.run_sync(_write_file_sync, full_path, content)
+
+
+def _write_file_sync(full_path: Path, content: FileContents) -> None:
+ full_path.parent.mkdir(parents=True, exist_ok=True)
+ if isinstance(content, bytes):
+ full_path.write_bytes(content)
+ elif isinstance(content, SymlinkContents):
+ full_path.symlink_to(content.target_path)
+ else:
+ logger.error(
+ "Tried to write contents that were neither bytes nor SymlinkContents: {content}",
+ content=content,
+ )
+
+
+@asynccontextmanager
+async def temporary_local_dir_from_in_memory_file_system(
+ file_contents: InMemoryFileSystem,
+) -> AsyncGenerator[str, None]:
+ with tempfile.TemporaryDirectory() as temp_dir:
+ await write_file_contents_to_dir(file_contents, temp_dir)
+ yield temp_dir
+
+
+def create_initial_placeholder_commit_for_dir(repo: pygit2.Repository) -> pygit2.Commit:
+ # pyre-ignore[16]: pyre doesn't understand the inheritance of Repository from BaseRepository
+ repo_index = repo.index
+ repo_index.add_all()
+ repo_index.write()
+ tree = repo_index.write_tree()
+ signature = pygit2.Signature("placeholder", "placeholder@example.com")
+
+ commit_oid = repo.create_commit(
+ "refs/heads/master",
+ signature,
+ signature,
+ "placeholder commit for diff utils",
+ tree,
+ [],
+ )
+ # pyre-ignore[16]: pyre doesn't understand the inheritance of Repository from BaseRepository
+ commit = repo.get(commit_oid)
+ return cast(pygit2.Commit, commit)
diff --git a/vet/imbue_tools/repo_utils/project_context.py b/vet/imbue_tools/repo_utils/project_context.py
@@ -0,0 +1,234 @@
+from functools import cached_property
+from functools import lru_cache
+from pathlib import Path
+from typing import Annotated
+from typing import Self
+
+import jinja2
+from pydantic import Tag
+from pydantic import computed_field
+
+from vet.imbue_core.agents.configs import LanguageModelGenerationConfig
+from vet.imbue_core.agents.configs import OpenAICompatibleModelConfig
+from vet.imbue_core.async_utils import sync
+from vet.imbue_core.frozen_utils import FrozenDict
+from vet.imbue_core.pydantic_serialization import SerializableModel
+from vet.imbue_core.pydantic_serialization import build_discriminator
+from vet.imbue_tools.repo_utils.context_prefix import StrategyMode
+from vet.imbue_tools.repo_utils.context_prefix import SubrepoContext
+from vet.imbue_tools.repo_utils.context_prefix import SubrepoContextWithFormattedContext
+from vet.imbue_tools.repo_utils.context_prefix import create_context_prompt_prefix
+from vet.imbue_tools.repo_utils.context_prefix import get_repo_context
+from vet.imbue_tools.repo_utils.context_retrieval import RepoContextManager
+from vet.imbue_tools.repo_utils.file_system import InMemoryFileSystem
+from vet.imbue_tools.repo_utils.subrepo_formatting import (
+ REPO_CONTEXT_TEMPLATE_WITH_NO_MENTION_OF_DIFF,
+)
+
+
+@lru_cache
+def _get_repo_context_manager_for_repo_path(repo_path: Path) -> RepoContextManager:
+ """
+ Wrapper around RepoContextManager.build() to cache the resulting repo context manager.
+
+ Internally, the RepoContextManager object will itself cache the repo contents.
+ """
+ return RepoContextManager.build(repo_path)
+
+
+class BaseProjectContext(SerializableModel):
+ """
+ Holds the context of the checked project including all its files.
+
+ For LLM-based issue identifiers, no matter the scope, we want to use a fixed prompt prefix to leverage caching.
+ We use the stable cached_prompt_prefix for that purpose.
+
+ """
+
+ object_type: str = "BaseProjectContext"
+
+ file_contents_by_path: FrozenDict[str, str]
+ cached_prompt_prefix: str
+ # 0 - n most recent commits, with the most recent one being the first.
+ # The state of the project (file_contents_by_path) is the state after the most recent commit.
+
+ subrepo_context: SubrepoContext | None = None
+ instruction_context: SubrepoContext | None = None
+ repo_path: Path | None = None
+
+ def get_file_contents(self, file_path: str) -> str | None:
+ return self.file_contents_by_path.get(file_path)
+
+ def get_computed_contexts(
+ self,
+ ) -> tuple[SubrepoContext | None, SubrepoContext | None]:
+ """To match usage for LazyProjectContext; all fields are always computed because this isn't lazy"""
+ return self.subrepo_context, self.instruction_context
+
+
+class LazyProjectContext(SerializableModel):
+ object_type: str = "LazyProjectContext"
+
+ base_commit: str
+ diff: str
+ language_model_name: str
+ repo_path: Path
+ tokens_to_reserve: int
+
+ # Optional context window override. If not provided, the model's default context window
+ # will be looked up from the model registry (which fails for unknown models).
+ context_window: int | None = None
+
+ # If True, this is a custom/user-defined model (uses approximate token counting).
+ is_custom_model: bool = False
+
+ def get_file_contents(self, file_path: str) -> str | None:
+ return self.file_contents_by_path.get(file_path)
+
+ @classmethod
+ def build(
+ cls,
+ base_commit: str,
+ diff: str,
+ language_model_name: str,
+ repo_path: Path,
+ # How many tokens to keep for the vet specific prompt and any output tokens.
+ tokens_to_reserve: int,
+ context_window: int | None = None,
+ is_custom_model: bool = False,
+ ) -> Self:
+ return cls(
+ base_commit=base_commit,
+ diff=diff,
+ language_model_name=language_model_name,
+ repo_path=repo_path,
+ tokens_to_reserve=tokens_to_reserve,
+ context_window=context_window,
+ is_custom_model=is_custom_model,
+ )
+
+ # The fields are computed and cached because they are quite expensive to compute.
+ # We compose multiple repo_context prefixes, and also have to tokenize them to check their respective lengths.
+ # Both of these operations are expensive
+
+ @computed_field
+ @cached_property
+ def repo_context_manager(self) -> RepoContextManager:
+ return _get_repo_context_manager_for_repo_path(self.repo_path)
+
+ @computed_field
+ @cached_property
+ def original_content_by_path(self) -> InMemoryFileSystem:
+ original_content_by_path = sync(self.repo_context_manager.get_full_repo_contents_at_commit)(self.base_commit)
+ return original_content_by_path
+
+ @computed_field
+ @cached_property
+ def content_by_path(self) -> InMemoryFileSystem:
+ if self.diff:
+ return sync(self.repo_context_manager.get_full_repo_contents_at_repo_state)(self.base_commit, self.diff)
+ else:
+ return self.original_content_by_path
+
+ @computed_field
+ @property
+ def file_contents_by_path(self) -> FrozenDict[str, str]:
+ return self.content_by_path.text_files
+
+ @computed_field
+ @cached_property
+ def cached_prompt_prefix(self) -> str:
+ prompt_prefix_template, prompt_prefix_params = create_context_prompt_prefix(
+ repo_context=self.subrepo_context.formatted_repo_context,
+ )
+ env = jinja2.Environment(undefined=jinja2.StrictUndefined)
+ jinja_template = env.from_string(prompt_prefix_template)
+ cached_prompt_prefix = jinja_template.render(**prompt_prefix_params)
+ return cached_prompt_prefix
+
+ @computed_field
+ @cached_property
+ def modified_file_paths(self) -> frozenset[str]:
+ modified_file_paths = []
+ for file_path in self.content_by_path.files.keys():
+ if self.content_by_path.get(file_path) != self.original_content_by_path.get(file_path):
+ modified_file_paths.append(file_path)
+ return frozenset(modified_file_paths)
+
+ def _create_model_config(self) -> LanguageModelGenerationConfig:
+ """Create the appropriate model config for context building.
+
+ For custom models (is_custom_model=True), creates an OpenAICompatibleModelConfig
+ that uses approximate token counting and the specified context window.
+
+ For known models, creates a standard LanguageModelGenerationConfig that uses
+ the model registry for token counting and context window lookup.
+ """
+ if self.is_custom_model:
+ if self.context_window is None:
+ raise ValueError(
+ "context_window must be provided when is_custom_model=True "
+ + "(custom models don't have a known context window)"
+ )
+ return OpenAICompatibleModelConfig(
+ model_name=self.language_model_name,
+ custom_base_url="", # Not used for context building
+ custom_api_key_env="", # Not used for context building
+ custom_context_window=self.context_window,
+ custom_max_output_tokens=0, # Not used for context building
+ )
+ else:
+ return LanguageModelGenerationConfig(model_name=self.language_model_name)
+
+ @computed_field
+ @cached_property
+ def subrepo_context(self) -> SubrepoContextWithFormattedContext:
+ model_config = self._create_model_config()
+
+ subrepo_context = get_repo_context(
+ full_repo_contents=self.content_by_path,
+ model_config=model_config,
+ relevant_file_paths=self.modified_file_paths,
+ tokens_to_reserve=self.tokens_to_reserve,
+ )
+ return subrepo_context
+
+ def to_base_project_context(self) -> BaseProjectContext:
+ return BaseProjectContext(
+ file_contents_by_path=self.file_contents_by_path,
+ cached_prompt_prefix=self.cached_prompt_prefix,
+ subrepo_context=self.subrepo_context,
+ instruction_context=self.instruction_context,
+ repo_path=self.repo_path,
+ )
+
+ @computed_field
+ @cached_property
+ def instruction_context(self) -> SubrepoContextWithFormattedContext:
+ model_config = self._create_model_config()
+ return get_repo_context(
+ model_config=model_config,
+ full_repo_contents=self.content_by_path,
+ relevant_file_paths=None,
+ tokens_to_reserve=self.tokens_to_reserve,
+ strategy_mode=StrategyMode.DOCS,
+ template=REPO_CONTEXT_TEMPLATE_WITH_NO_MENTION_OF_DIFF,
+ )
+
+ def get_computed_contexts(
+ self,
+ ) -> tuple[
+ SubrepoContextWithFormattedContext | None,
+ SubrepoContextWithFormattedContext | None,
+ ]:
+ """Returns subrepo context and instruction context, but only if they have already been computed; those that haven't been computed are None"""
+ # checking for presence in __dict__ does not trigger computation
+ subrepo_context = self.subrepo_context if "subrepo_context" in self.__dict__ else None
+ instruction_context = self.instruction_context if "instruction_context" in self.__dict__ else None
+ return subrepo_context, instruction_context
+
+
+ProjectContext = Annotated[
+ Annotated[BaseProjectContext, Tag("BaseProjectContext")] | LazyProjectContext,
+ build_discriminator(),
+]
diff --git a/vet/imbue_tools/repo_utils/python_imports.py b/vet/imbue_tools/repo_utils/python_imports.py
@@ -0,0 +1,130 @@
+import ast
+import sys
+from pathlib import Path
+
+from vet.imbue_core.pydantic_serialization import SerializableModel
+
+STANDARD_LIBRARIES: frozenset[str] = sys.stdlib_module_names | frozenset(sys.builtin_module_names)
+
+
+class QualifiedName(SerializableModel):
+ """A qualified name like 'foo.bar.baz'."""
+
+ value: str
+
+ @property
+ def top_level_name(self) -> "QualifiedName":
+ """Return the top-level module name (e.g., 'foo' from 'foo.bar.baz')."""
+ return QualifiedName(value=self.value.split(".", maxsplit=1)[0])
+
+ @property
+ def parent_name(self) -> "QualifiedName":
+ """Return the parent module name (e.g., 'foo.bar' from 'foo.bar.baz')."""
+ return QualifiedName(value=self.value.rsplit(".", maxsplit=1)[0])
+
+ def to_path(self) -> Path:
+ """Convert qualified name to a file path (e.g., 'foo.bar' -> 'foo/bar.py')."""
+ return Path(self.value.replace(".", "/") + ".py")
+
+
+class Import(SerializableModel):
+ """Represents a single import statement."""
+
+ source: str
+ alias: str | None
+ qualified_name: QualifiedName
+
+
+def _collect_global_imports(node: ast.AST, imports: list[Import]) -> None:
+ """
+ Recursively collect import statements at global scope.
+
+ Stops recursing into function and class definitions since imports
+ inside those are not at global scope.
+
+ Args:
+ node: The AST node to process
+ imports: List to accumulate found imports
+ """
+ if isinstance(node, ast.Import):
+ # Handle: import foo, bar
+ # Handle: import foo as bar
+ for alias in node.names:
+ if alias.asname:
+ source = f"import {alias.name} as {alias.asname}"
+ alias_name = alias.asname
+ else:
+ source = f"import {alias.name}"
+ alias_name = None
+ imports.append(
+ Import(
+ source=source,
+ alias=alias_name,
+ qualified_name=QualifiedName(value=alias.name),
+ )
+ )
+ elif isinstance(node, ast.ImportFrom):
+ # Handle: from foo import bar, baz
+ module = node.module or ""
+ if node.names[0].name == "*":
+ # from foo import *
+ source = f"from {module} import *"
+ imports.append(
+ Import(
+ source=source,
+ alias=None,
+ qualified_name=QualifiedName(value=f"{module}.*"),
+ )
+ )
+ else:
+ for alias in node.names:
+ if module:
+ full_name = f"{module}.{alias.name}"
+ else:
+ # relative import like: from . import foo
+ full_name = alias.name
+
+ if alias.asname:
+ source = f"from {module} import {alias.name} as {alias.asname}"
+ alias_name = alias.asname
+ else:
+ source = f"from {module} import {alias.name}"
+ alias_name = None
+
+ imports.append(
+ Import(
+ source=source,
+ alias=alias_name,
+ qualified_name=QualifiedName(value=full_name),
+ )
+ )
+
+ # Don't recurse into function or class definitions - imports there are not global
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Lambda)):
+ return
+
+ # Recurse into child nodes
+ for child in ast.iter_child_nodes(node):
+ _collect_global_imports(child, imports)
+
+
+def get_global_imports(source_code: str) -> tuple[Import, ...]:
+ """
+ Extract all global imports from Python source code.
+
+ This includes imports at module level, as well as imports inside conditionals
+ or other control flow structures at the top level (not inside functions or classes).
+
+ Args:
+ source_code: Python source code as a string
+
+ Returns:
+ Tuple of Import objects representing all imports in the file
+
+ Raises:
+ SyntaxError: If the source code cannot be parsed
+ """
+ tree = ast.parse(source_code)
+ imports: list[Import] = []
+ _collect_global_imports(tree, imports)
+ return tuple(imports)
diff --git a/imbue_tools/imbue_tools/repo_utils/stubify_file.py b/vet/imbue_tools/repo_utils/stubify_file.py
diff --git a/vet/imbue_tools/repo_utils/subrepo_formatting.py b/vet/imbue_tools/repo_utils/subrepo_formatting.py
@@ -0,0 +1,345 @@
+import functools
+from enum import Enum
+from typing import Annotated
+from typing import Iterable
+from typing import Mapping
+from typing import Self
+from typing import assert_never
+
+import jinja2
+from pathspec import GitIgnoreSpec
+from pydantic import Tag
+
+from vet.imbue_core.agents.configs import LanguageModelGenerationConfig
+from vet.imbue_core.pydantic_serialization import SerializableModel
+from vet.imbue_core.pydantic_serialization import build_discriminator
+from vet.imbue_tools.repo_utils.context_utils import escape_all_jinja_variables
+from vet.imbue_tools.repo_utils.context_utils import escape_prompt_markers
+from vet.imbue_tools.repo_utils.data_types import FileContext
+from vet.imbue_tools.repo_utils.data_types import FileContextUnion
+from vet.imbue_tools.repo_utils.data_types import FilenameContext
+from vet.imbue_tools.repo_utils.data_types import FullFileContext
+from vet.imbue_tools.repo_utils.data_types import StubFileContext
+from vet.imbue_tools.repo_utils.errors import ContextLengthExceededError
+from vet.imbue_tools.repo_utils.stubify_file import stubify_code_file
+
+
+class ContextFormatStyle(Enum):
+ FULL_FILE = "FULL_FILE"
+ STUB = "STUB"
+ FILENAME_ONLY = "FILENAME_ONLY"
+ HIDDEN = "HIDDEN"
+
+
+class BaseFilenamePattern(SerializableModel):
+ """
+ Extends the functionality of `GitIgnoreSpec` to be serializable.
+ """
+
+ object_type: str = "BaseFilenamePattern"
+ lines: tuple[str, ...]
+
+ @functools.cached_property
+ def git_ignore_spec(self) -> GitIgnoreSpec:
+ return GitIgnoreSpec.from_lines(self.lines)
+
+ @classmethod
+ def from_lines(cls, lines: Iterable[str]) -> Self:
+ return cls(lines=tuple(sorted(lines)))
+
+ def match_file(self, file: str) -> bool:
+ return self.git_ignore_spec.match_file(file)
+
+
+class NegatedFilenamePattern(BaseFilenamePattern):
+ """Matches everything except the files matched by the base pattern."""
+
+ object_type: str = "NegatedFilenamePattern"
+
+ def match_file(self, file: str) -> bool:
+ return not self.git_ignore_spec.match_file(file)
+
+ @classmethod
+ def build_from_positive_pattern(cls, positive_pattern: BaseFilenamePattern) -> Self:
+ return cls(lines=positive_pattern.lines)
+
+
+class IntersectionFilenamePattern(SerializableModel):
+ object_type: str = "IntersectionFilenamePattern"
+ specs: tuple["FilenamePattern", ...]
+
+ def match_file(self, file: str) -> bool:
+ return all(spec.match_file(file) for spec in self.specs)
+
+
+class UnionFilenamePattern(SerializableModel):
+ object_type: str = "UnionFilenamePattern"
+ specs: tuple["FilenamePattern", ...]
+
+ def match_file(self, file: str) -> bool:
+ return any(spec.match_file(file) for spec in self.specs)
+
+
+class ExactFilenamePattern(SerializableModel):
+ """
+ Similar to a BaseFilenamePattern, but more efficient thanks to the use of a hash set.
+ However, it only supports exact filename matches and no patterns.
+
+ O(1) matching over n filenames, instead of O(n) with BaseFilenamePattern.
+ """
+
+ object_type: str = "ExactFilenamePattern"
+ # Will match these exact filenames.
+ # We store this as a tuple instead of frozenset to have deterministic ordering. Helps with snapshot tests.
+ filenames: tuple[str, ...]
+
+ def match_file(self, file: str) -> bool:
+ return file in self.filenames_set
+
+ @functools.cached_property
+ def filenames_set(self) -> set[str]:
+ return set(self.filenames)
+
+
+FilenamePattern = Annotated[
+ Annotated[BaseFilenamePattern, Tag("BaseFilenamePattern")]
+ | Annotated[NegatedFilenamePattern, Tag("NegatedFilenamePattern")]
+ | Annotated[IntersectionFilenamePattern, Tag("IntersectionFilenamePattern")]
+ | Annotated[UnionFilenamePattern, Tag("UnionFilenamePattern")]
+ | Annotated[ExactFilenamePattern, Tag("ExactFilenamePattern")],
+ build_discriminator(),
+]
+
+
+SubrepoContextMatchers = tuple[tuple[ContextFormatStyle, FilenamePattern], ...]
+
+
+@functools.lru_cache(maxsize=100)
+def stubify_file_contents_cached(path: str, contents: str) -> str:
+ # TODO: there's various flags here we could try
+ # TODO: we may want an option to suppress comments, which end up being a large percent of the lines
+ if path.endswith(".py"):
+ return stubify_code_file(path, contents, keep_indent=True)
+ else:
+ # For non-Python files, maintain the full contents for now.
+ return contents
+
+
+def stubify_and_format_for_agent_context(path: str, contents: str | None) -> StubFileContext:
+ contents_to_use = contents if contents is not None else "RAW FILE CONTENTS"
+ stub = stubify_file_contents_cached(path=path, contents=contents_to_use)
+ return StubFileContext(path=path, stub=stub)
+
+
+def format_filename_only_for_agent_context(path: str) -> FilenameContext:
+ return FilenameContext(path=path)
+
+
+def format_full_file_for_agent_context(path: str, contents: str) -> FullFileContext:
+ return FullFileContext(path=path, contents=contents)
+
+
+BASE_REPO_CONTEXT_TEMPLATE = """
+<REPO_CONTEXT>
+{{repo_context}}
+</REPO_CONTEXT>
+"""
+
+REPO_CONTEXT_TEMPLATE_WITH_NO_MENTION_OF_DIFF = (
+ """
+{% if not is_shortened %}For context, here are the contents of all files currently in the project:
+{% else %}The project's repository is too large to show in full, so we have chosen a useful subset for your context.
+Files in the repository may be shown in either full, stubified, or filename-only form{% if has_hidden_files %}, or they may be hidden entirely{% endif %}. Here are the contents of some of the files in the repository:{% endif %}
+"""
+ + BASE_REPO_CONTEXT_TEMPLATE
+)
+
+REPO_CONTEXT_TEMPLATE = (
+ REPO_CONTEXT_TEMPLATE_WITH_NO_MENTION_OF_DIFF
+ + """
+
+If any files have been changed, the changes will be described next. Otherwise, you can assume this is the current state of the project.
+"""
+)
+
+
+def format_file_for_agent_context(
+ path: str, contents: str, format_style: ContextFormatStyle
+) -> FileContextUnion | None:
+ if format_style == ContextFormatStyle.FULL_FILE:
+ return format_full_file_for_agent_context(path, contents)
+ elif format_style == ContextFormatStyle.STUB:
+ return stubify_and_format_for_agent_context(path, contents)
+ elif format_style == ContextFormatStyle.FILENAME_ONLY:
+ return format_filename_only_for_agent_context(path)
+ elif format_style == ContextFormatStyle.HIDDEN:
+ return None
+ else:
+ assert_never(format_style) # pyre-ignore[6]: pyre doesn't understand enums
+
+
+@functools.lru_cache(maxsize=20)
+def parse_subrepo_context_matchers_from_toml(
+ subrepo_context_config_toml: str,
+) -> SubrepoContextMatchers:
+ current_mode: ContextFormatStyle
+ matchers = []
+ for line in subrepo_context_config_toml.splitlines():
+ line = line.strip()
+ if not line:
+ continue
+ if line.startswith("["):
+ current_mode = ContextFormatStyle[line.replace("[", "").replace("]", "").upper()] # type: ignore
+ continue
+
+ exclude_spec = BaseFilenamePattern.from_lines([line])
+ matchers.append((current_mode, exclude_spec))
+ return tuple(matchers)
+
+
+def compute_file_format_style(
+ file_path: str,
+ subrepo_context_matchers: SubrepoContextMatchers,
+ exclusions: FilenamePattern | None = None,
+) -> ContextFormatStyle:
+ for matcher_format, matcher in subrepo_context_matchers:
+ if matcher.match_file(file_path):
+ if exclusions and matcher_format != ContextFormatStyle.HIDDEN and exclusions.match_file(file_path):
+ # Exclusions override any non-hidden format and downgrade it to FILENAME_ONLY.
+ return ContextFormatStyle.FILENAME_ONLY
+ # Return the first match
+ return matcher_format
+ return ContextFormatStyle.HIDDEN
+
+
+def compute_file_context_format_styles(
+ file_paths: Iterable[str],
+ subrepo_context_matchers: SubrepoContextMatchers,
+ exclusions: FilenamePattern | None = None,
+) -> Mapping[str, ContextFormatStyle]:
+ return {
+ file_path: compute_file_format_style(file_path, subrepo_context_matchers, exclusions)
+ for file_path in file_paths
+ }
+
+
+def get_estimated_lower_bound_token_count_for_text_and_model(text: str, model_name: str) -> int:
+ # A factor of 1/4.5 appears to be a reasonable empirical estimate for current models.
+ # We use a slighly smaller factor (1/5) to give more of a lower bound estimate.
+ return round(len(text) / 5)
+
+
+def format_all_for_agent(repo_contents: tuple[FileContext, ...]) -> dict[str, str]:
+ return {contents.path: contents.format_for_agent() for contents in repo_contents}
+
+
+def format_subrepo(formatted_repo_contents: Mapping[str, str]) -> str:
+ repo_context_str = "".join([contents for contents in formatted_repo_contents.values() if contents is not None])
+ return escape_all_jinja_variables(escape_prompt_markers(repo_context_str))
+
+
+def formatted_subrepo_to_prompt(
+ repo_context_str: str, is_shortened: bool, has_hidden_files: bool, template: str
+) -> str:
+ env = jinja2.Environment(undefined=jinja2.StrictUndefined)
+ jinja_template = env.from_string(template)
+ repo_context_prompt = jinja_template.render(
+ repo_context=repo_context_str,
+ is_shortened=is_shortened,
+ has_hidden_files=has_hidden_files,
+ )
+ return repo_context_prompt
+
+
+def format_subrepo_context_into_filecontexts(
+ full_repo_contents: Mapping[str, str],
+ path_to_format_style: Mapping[str, ContextFormatStyle],
+) -> tuple[FileContextUnion, ...]:
+ repo_contents = tuple(
+ format_file_for_agent_context(path, contents, path_to_format_style[path])
+ for path, contents in full_repo_contents.items()
+ )
+ repo_contents_with_hidden_removed = tuple(contents for contents in repo_contents if contents is not None)
+ return repo_contents_with_hidden_removed
+
+
+def build_context_from_filecontexts(
+ repo_contents_with_hidden_removed: tuple[FileContextUnion, ...],
+ model_config: LanguageModelGenerationConfig,
+ # how many tokens to reserve for additional prompt messages and output
+ tokens_to_reserve: int,
+ template: str = REPO_CONTEXT_TEMPLATE,
+ path_to_format_style: Mapping[str, ContextFormatStyle] | None = None,
+) -> str:
+ """
+ Returns the repo contents formatted according to the format styles as a string.
+ Includes (at least if the default template is used) an explanation at the beginning
+ saying that these are the contents of the repo, potentially truncated or hidden.
+
+ If there are no repo contents, returns an empty string.
+ """
+
+ if not repo_contents_with_hidden_removed:
+ return ""
+
+ max_context_length = model_config.get_max_context_length()
+ available_tokens = max_context_length - tokens_to_reserve
+
+ formatted_repo_contents_with_hidden_removed = format_all_for_agent(repo_contents_with_hidden_removed)
+
+ repo_context_str = format_subrepo(formatted_repo_contents_with_hidden_removed)
+
+ if model_config.is_custom_model():
+ # For custom models, approximate_token_count is already fast, so skip the estimation step.
+ full_repo_context_token_count = model_config.count_tokens(repo_context_str)
+ else:
+ # First use an estimation of the token count to see if we are likely below the maximum length. Then
+ # double-check with the exact token count.
+ # We do this because getting the exact token count is quite slow.
+ estimated_full_repo_context_token_count = get_estimated_lower_bound_token_count_for_text_and_model(
+ repo_context_str, model_config.model_name
+ )
+ if estimated_full_repo_context_token_count > available_tokens:
+ raise ContextLengthExceededError(
+ f"Estimated context has size {estimated_full_repo_context_token_count}; available tokens {available_tokens}"
+ )
+ full_repo_context_token_count = model_config.count_tokens(repo_context_str)
+ if full_repo_context_token_count > available_tokens:
+ raise ContextLengthExceededError(
+ f"Context has size {full_repo_context_token_count}; available tokens {available_tokens}"
+ )
+
+ if path_to_format_style is None:
+ path_to_format_style = {
+ contents.path: ContextFormatStyle.FULL_FILE for contents in repo_contents_with_hidden_removed
+ }
+
+ is_shortened = any([style != ContextFormatStyle.FULL_FILE for style in path_to_format_style.values()])
+ has_hidden_files = any([style == ContextFormatStyle.HIDDEN for style in path_to_format_style.values()])
+
+ repo_context_prompt = formatted_subrepo_to_prompt(
+ repo_context_str=repo_context_str,
+ is_shortened=is_shortened,
+ has_hidden_files=has_hidden_files,
+ template=template,
+ )
+ return repo_context_prompt
+
+
+def format_subrepo_context(
+ full_repo_contents: Mapping[str, str],
+ path_to_format_style: Mapping[str, ContextFormatStyle],
+ model_config: LanguageModelGenerationConfig,
+ # how many tokens to reserve for additional prompt messages and output
+ tokens_to_reserve: int,
+ template: str = REPO_CONTEXT_TEMPLATE,
+) -> tuple[str, tuple[FileContextUnion, ...]]:
+ repo_contents_tuple = format_subrepo_context_into_filecontexts(full_repo_contents, path_to_format_style)
+ repo_context_str = build_context_from_filecontexts(
+ repo_contents_tuple,
+ model_config,
+ tokens_to_reserve,
+ template,
+ path_to_format_style,
+ )
+ return repo_context_str, repo_contents_tuple
diff --git a/vet/imbue_tools/types/vet_config.py b/vet/imbue_tools/types/vet_config.py
@@ -0,0 +1,100 @@
+from pathlib import Path
+
+from vet.imbue_core.agents.configs import LanguageModelGenerationConfig
+from vet.imbue_core.agents.llm_apis.anthropic_api import AnthropicModelName
+from vet.imbue_core.data_types import IssueCode
+from vet.imbue_core.pydantic_serialization import SerializableModel
+
+DEFAULT_CONFIDENCE_THRESHOLD = 0.8
+
+
+class VetConfig(SerializableModel):
+ """Configuration for the vet system."""
+
+ # If none, all registered identifiers are used.
+ # Otherwise, only the identifiers in this tuple are used.
+ enabled_identifiers: tuple[str, ...] | None = None
+
+ # Issue identifiers that are disabled are never used.
+ disabled_identifiers: tuple[str, ...] | None = None
+
+ # Similar to the above, but for reporting specific types of issues.
+ # (Use the values from the vet.data_types.IssueCode enum.)
+ enabled_issue_codes: tuple[IssueCode, ...] | None = None
+ disabled_issue_codes: tuple[IssueCode, ...] | None = ()
+
+ # Todo: Different models for different issue identifiers
+ language_model_generation_config: LanguageModelGenerationConfig = LanguageModelGenerationConfig(
+ model_name=AnthropicModelName.CLAUDE_4_5_HAIKU_2025_10_01
+ )
+ max_identifier_spend_dollars: float = 5.0
+ max_output_tokens: int = 20000
+ enable_parallel_agentic_issue_identification: bool = False
+ max_identify_workers: int | None = None
+ temperature: float = 0.5
+
+ # If True, apply an additional LLM-based filtering stage, where each identified issue is evaluated
+ # according to a number of quality criteria. Only issues that pass the evaluation are returned.
+ filter_issues: bool = True
+ filter_issues_through_llm_evaluator: bool = True
+ filter_issues_below_confidence: float | None = DEFAULT_CONFIDENCE_THRESHOLD
+
+ enable_deduplication: bool = True
+ enable_collation: bool = True
+
+ # If True, we attempt to cache the full prompts including specific inputs with the LLM provider.
+ # There can be an additional cost for such a cache write, but it can help save cost in evaluation
+ # contexts (such as black_box_evals) where the same inputs are being evaluated multiple times.
+ cache_full_prompt: bool = False
+
+ extra_context: str | None = None
+
+ @classmethod
+ def build(
+ cls,
+ language_model_name: str | None = None,
+ language_model_cache_path: Path | None = None,
+ enabled_identifiers: tuple[str, ...] | None = None,
+ enable_parallel_agentic_issue_identification: bool = False,
+ max_identify_workers: int | None = None,
+ filter_issues: bool = True,
+ filter_issues_below_confidence: float | None = DEFAULT_CONFIDENCE_THRESHOLD,
+ enable_deduplication: bool = True,
+ enable_collation: bool = True,
+ enabled_issue_codes: tuple[IssueCode, ...] | None = None,
+ temperature: float = 0.5,
+ retry_jitter_factor: float = 0.0,
+ cache_full_prompt: bool = False,
+ ) -> "VetConfig":
+ if not language_model_name:
+ language_model_name = AnthropicModelName.CLAUDE_4_5_HAIKU_2025_10_01
+ language_model_generation_config = LanguageModelGenerationConfig(
+ model_name=language_model_name,
+ cache_path=language_model_cache_path,
+ retry_jitter_factor=retry_jitter_factor,
+ )
+ return cls(
+ language_model_generation_config=language_model_generation_config,
+ enabled_identifiers=enabled_identifiers,
+ enable_parallel_agentic_issue_identification=enable_parallel_agentic_issue_identification,
+ max_identify_workers=max_identify_workers,
+ filter_issues=filter_issues,
+ filter_issues_below_confidence=filter_issues_below_confidence,
+ enable_deduplication=enable_deduplication,
+ enable_collation=enable_collation,
+ enabled_issue_codes=enabled_issue_codes,
+ temperature=temperature,
+ cache_full_prompt=cache_full_prompt,
+ )
+
+
+def get_enabled_issue_codes(config: VetConfig) -> set[IssueCode]:
+ all_issue_code_values = {item.value for item in IssueCode}
+ explicitly_enabled = config.enabled_issue_codes or tuple()
+ explicitly_disabled = config.disabled_issue_codes or tuple()
+ for code in explicitly_enabled + explicitly_disabled:
+ if code not in all_issue_code_values:
+ raise ValueError(f"Bad config: unknown issue code: {code}")
+ possibly_enabled_values = set(explicitly_enabled) if len(explicitly_enabled) > 0 else set(v for v in IssueCode)
+ disabled_values = set(explicitly_disabled)
+ return possibly_enabled_values - disabled_values
diff --git a/imbue_tools/imbue_tools/util_prompts/conversation_prefix.py b/vet/imbue_tools/util_prompts/conversation_prefix.py
diff --git a/vet/imbue_tools/util_prompts/goal_from_conversation.py b/vet/imbue_tools/util_prompts/goal_from_conversation.py
@@ -0,0 +1,64 @@
+import jinja2
+
+from vet.imbue_core.agents.configs import LanguageModelGenerationConfig
+from vet.imbue_core.agents.llm_apis.build_apis import build_language_model_from_config
+from vet.imbue_core.agents.llm_apis.data_types import CostedLanguageModelResponse
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelGenerationParams
+from vet.imbue_core.itertools import only
+from vet.vet_types.messages import ConversationMessageUnion
+from vet.imbue_tools.get_conversation_history.get_conversation_history import (
+ format_conversation_history_for_prompt,
+)
+from vet.imbue_tools.util_prompts.conversation_prefix import CONVERSATION_PREFIX_TEMPLATE
+
+# TODO: see how this does on actual examples where the agent did something other than what the user asked for
+PROMPT_TEMPLATE = (
+ CONVERSATION_PREFIX_TEMPLATE
+ + """
+[ROLE=USER]
+What is the user's goal based on the preceding conversation?
+Pay attention only to what the user asks for, not what the agent does.
+Respond with a brief description of the goal--a few sentences at most.
+The goal should be listed as an imperative; for example "Implement XYZ" rather than "The user's goal is to implement XYZ".
+Do not include any reasoning or other text in your response.
+"""
+)
+
+# should be totally sufficient for a goal that's only supposed to be a few sentences
+MAX_OUTPUT_TOKENS = 500
+
+GOAL_GENERATION_DEFAULT_PARAMS = LanguageModelGenerationParams(temperature=0.0, max_tokens=MAX_OUTPUT_TOKENS)
+
+
+def prompt_for_getting_goal_from_conversation(
+ conversation_history: tuple[ConversationMessageUnion, ...],
+) -> str:
+ env = jinja2.Environment(undefined=jinja2.StrictUndefined)
+ jinja_template = env.from_string(PROMPT_TEMPLATE)
+ return jinja_template.render(conversation_history=format_conversation_history_for_prompt(conversation_history))
+
+
+def get_goal_from_conversation_with_usage(
+ conversation_history: tuple[ConversationMessageUnion, ...],
+ language_model_generation_config: LanguageModelGenerationConfig,
+) -> CostedLanguageModelResponse:
+ """Query an LLM with the conversation history to get the user's goal, and include usage info in the response."""
+ language_model = build_language_model_from_config(language_model_generation_config)
+ prompt = prompt_for_getting_goal_from_conversation(conversation_history)
+ costed_response = language_model.complete_with_usage_sync(
+ prompt,
+ params=GOAL_GENERATION_DEFAULT_PARAMS,
+ is_caching_enabled=language_model.cache_path is not None,
+ )
+ return costed_response
+
+
+def get_goal_from_conversation(
+ conversation_history: tuple[ConversationMessageUnion, ...],
+ language_model_generation_config: LanguageModelGenerationConfig,
+) -> str:
+ """Query an LLM with the conversation history to get the user's goal."""
+ response = only(
+ get_goal_from_conversation_with_usage(conversation_history, language_model_generation_config).responses
+ )
+ return response.text
diff --git a/vet/issue_identifiers/agentic_issue_collation.py b/vet/issue_identifiers/agentic_issue_collation.py
@@ -4,19 +4,19 @@ from typing import Iterable
import jinja2
-from imbue_core.data_types import AgenticPhase
-from imbue_core.data_types import IssueCode
-from imbue_core.data_types import IssueIdentificationDebugInfo
-from imbue_core.data_types import IssueIdentificationLLMResponseMetadata
-from imbue_core.data_types import LLMResponse
-from imbue_tools.get_conversation_history.input_data_types import CommitInputs
-from imbue_tools.get_conversation_history.input_data_types import IdentifierInputs
-from imbue_tools.get_conversation_history.input_data_types import (
+from vet.imbue_core.data_types import AgenticPhase
+from vet.imbue_core.data_types import IssueCode
+from vet.imbue_core.data_types import IssueIdentificationDebugInfo
+from vet.imbue_core.data_types import IssueIdentificationLLMResponseMetadata
+from vet.imbue_core.data_types import LLMResponse
+from vet.imbue_tools.get_conversation_history.input_data_types import CommitInputs
+from vet.imbue_tools.get_conversation_history.input_data_types import IdentifierInputs
+from vet.imbue_tools.get_conversation_history.input_data_types import (
to_specific_inputs_type,
)
-from imbue_tools.repo_utils.context_utils import escape_prompt_markers
-from imbue_tools.repo_utils.project_context import ProjectContext
-from imbue_tools.types.vet_config import VetConfig
+from vet.imbue_tools.repo_utils.context_utils import escape_prompt_markers
+from vet.imbue_tools.repo_utils.project_context import ProjectContext
+from vet.imbue_tools.types.vet_config import VetConfig
from vet.issue_identifiers.common import GeneratedIssueSchema
from vet.issue_identifiers.common import GeneratedResponseSchema
from vet.issue_identifiers.common import extract_invocation_info_from_messages
diff --git a/vet/issue_identifiers/base.py b/vet/issue_identifiers/base.py
@@ -3,15 +3,15 @@ from typing import Generator
from typing import Generic
from typing import TypeVar
-from imbue_core.data_types import IssueCode
-from imbue_core.data_types import IssueIdentificationDebugInfo
-from imbue_core.pydantic_serialization import SerializableModel
-from imbue_tools.get_conversation_history.input_data_types import IdentifierInputs
-from imbue_tools.get_conversation_history.input_data_types import (
+from vet.imbue_core.data_types import IssueCode
+from vet.imbue_core.data_types import IssueIdentificationDebugInfo
+from vet.imbue_core.pydantic_serialization import SerializableModel
+from vet.imbue_tools.get_conversation_history.input_data_types import IdentifierInputs
+from vet.imbue_tools.get_conversation_history.input_data_types import (
to_specific_inputs_type,
)
-from imbue_tools.repo_utils.project_context import ProjectContext
-from imbue_tools.types.vet_config import VetConfig
+from vet.imbue_tools.repo_utils.project_context import ProjectContext
+from vet.imbue_tools.types.vet_config import VetConfig
from vet.issue_identifiers.common import GeneratedIssueSchema
T = TypeVar("T", bound=IdentifierInputs)
diff --git a/vet/issue_identifiers/common.py b/vet/issue_identifiers/common.py
@@ -11,34 +11,34 @@ from loguru import logger
from pydantic import Field
from pydantic import PrivateAttr
-from imbue_core.agents.agent_api.api import get_agent_client
-from imbue_core.agents.agent_api.claude.data_types import ClaudeCodeOptions
-from imbue_core.agents.agent_api.data_types import AgentAssistantMessage
-from imbue_core.agents.agent_api.data_types import AgentMessage
-from imbue_core.agents.agent_api.data_types import AgentResultMessage
-from imbue_core.agents.agent_api.data_types import AgentTextBlock
-from imbue_core.agents.agent_api.data_types import AgentToolName
-from imbue_core.agents.agent_api.data_types import READ_ONLY_TOOLS
-from imbue_core.agents.llm_apis.anthropic_data_types import AnthropicCachingInfo
-from imbue_core.agents.llm_apis.data_types import CostedLanguageModelResponse
-from imbue_core.async_monkey_patches import log_exception
-from imbue_core.data_types import ConfidenceScore
-from imbue_core.data_types import IdentifiedVerifyIssue
-from imbue_core.data_types import InvocationInfo
-from imbue_core.data_types import IssueCode
-from imbue_core.data_types import IssueIdentificationDebugInfo
-from imbue_core.data_types import IssueIdentifierResult
-from imbue_core.data_types import IssueLocation
-from imbue_core.data_types import LineRange
-from imbue_core.data_types import SeverityScore
-from imbue_core.pydantic_serialization import SerializableModel
-from imbue_tools.llm_output_parsing.parse_model_json_response import (
+from vet.imbue_core.agents.agent_api.api import get_agent_client
+from vet.imbue_core.agents.agent_api.claude.data_types import ClaudeCodeOptions
+from vet.imbue_core.agents.agent_api.data_types import AgentAssistantMessage
+from vet.imbue_core.agents.agent_api.data_types import AgentMessage
+from vet.imbue_core.agents.agent_api.data_types import AgentResultMessage
+from vet.imbue_core.agents.agent_api.data_types import AgentTextBlock
+from vet.imbue_core.agents.agent_api.data_types import AgentToolName
+from vet.imbue_core.agents.agent_api.data_types import READ_ONLY_TOOLS
+from vet.imbue_core.agents.llm_apis.anthropic_data_types import AnthropicCachingInfo
+from vet.imbue_core.agents.llm_apis.data_types import CostedLanguageModelResponse
+from vet.imbue_core.async_monkey_patches import log_exception
+from vet.imbue_core.data_types import ConfidenceScore
+from vet.imbue_core.data_types import IdentifiedVerifyIssue
+from vet.imbue_core.data_types import InvocationInfo
+from vet.imbue_core.data_types import IssueCode
+from vet.imbue_core.data_types import IssueIdentificationDebugInfo
+from vet.imbue_core.data_types import IssueIdentifierResult
+from vet.imbue_core.data_types import IssueLocation
+from vet.imbue_core.data_types import LineRange
+from vet.imbue_core.data_types import SeverityScore
+from vet.imbue_core.pydantic_serialization import SerializableModel
+from vet.imbue_tools.llm_output_parsing.parse_model_json_response import (
ResponseParsingError,
)
-from imbue_tools.llm_output_parsing.parse_model_json_response import (
+from vet.imbue_tools.llm_output_parsing.parse_model_json_response import (
parse_model_json_response,
)
-from imbue_tools.repo_utils.project_context import ProjectContext
+from vet.imbue_tools.repo_utils.project_context import ProjectContext
from vet.issue_identifiers.identification_guides import (
IssueIdentificationGuide,
)
diff --git a/vet/issue_identifiers/common_test.py b/vet/issue_identifiers/common_test.py
@@ -2,19 +2,19 @@ import json
from pathlib import Path
from typing import Iterable
-from imbue_core.async_monkey_patches_test import expect_exact_logged_errors
-from imbue_core.data_types import IdentifiedVerifyIssue
-from imbue_core.data_types import IssueCode
-from imbue_core.frozen_utils import FrozenDict
-from imbue_core.itertools import only
-from imbue_tools.llm_output_parsing.parse_model_json_response import (
+from vet.imbue_core.async_monkey_patches_test import expect_exact_logged_errors
+from vet.imbue_core.data_types import IdentifiedVerifyIssue
+from vet.imbue_core.data_types import IssueCode
+from vet.imbue_core.frozen_utils import FrozenDict
+from vet.imbue_core.itertools import only
+from vet.imbue_tools.llm_output_parsing.parse_model_json_response import (
ResponseParsingError,
)
-from imbue_tools.llm_output_parsing.parse_model_json_response import (
+from vet.imbue_tools.llm_output_parsing.parse_model_json_response import (
parse_model_json_response,
)
-from imbue_tools.repo_utils.project_context import BaseProjectContext
-from imbue_tools.repo_utils.project_context import ProjectContext
+from vet.imbue_tools.repo_utils.project_context import BaseProjectContext
+from vet.imbue_tools.repo_utils.project_context import ProjectContext
from vet.issue_identifiers.common import GeneratedResponseSchema
from vet.issue_identifiers.common import (
convert_generated_issue_to_identified_issue,
diff --git a/vet/issue_identifiers/harnesses/agentic.py b/vet/issue_identifiers/harnesses/agentic.py
@@ -12,21 +12,21 @@ from typing import Generator
import jinja2
from loguru import logger
-from imbue_core.agents.agent_api.claude.data_types import ClaudeCodeOptions
-from imbue_core.agents.agent_api.data_types import AgentMessage
-from imbue_core.agents.agent_api.data_types import AgentToolName
-from imbue_core.agents.agent_api.data_types import READ_ONLY_TOOLS
-from imbue_core.agents.llm_apis.anthropic_api import AnthropicModelName
-from imbue_core.async_monkey_patches import log_exception
-from imbue_core.data_types import AgenticPhase
-from imbue_core.data_types import IssueCode
-from imbue_core.data_types import IssueIdentificationDebugInfo
-from imbue_core.data_types import IssueIdentificationLLMResponseMetadata
-from imbue_core.data_types import LLMResponse
-from imbue_tools.get_conversation_history.input_data_types import CommitInputs
-from imbue_tools.repo_utils.context_utils import escape_prompt_markers
-from imbue_tools.repo_utils.project_context import ProjectContext
-from imbue_tools.types.vet_config import VetConfig
+from vet.imbue_core.agents.agent_api.claude.data_types import ClaudeCodeOptions
+from vet.imbue_core.agents.agent_api.data_types import AgentMessage
+from vet.imbue_core.agents.agent_api.data_types import AgentToolName
+from vet.imbue_core.agents.agent_api.data_types import READ_ONLY_TOOLS
+from vet.imbue_core.agents.llm_apis.anthropic_api import AnthropicModelName
+from vet.imbue_core.async_monkey_patches import log_exception
+from vet.imbue_core.data_types import AgenticPhase
+from vet.imbue_core.data_types import IssueCode
+from vet.imbue_core.data_types import IssueIdentificationDebugInfo
+from vet.imbue_core.data_types import IssueIdentificationLLMResponseMetadata
+from vet.imbue_core.data_types import LLMResponse
+from vet.imbue_tools.get_conversation_history.input_data_types import CommitInputs
+from vet.imbue_tools.repo_utils.context_utils import escape_prompt_markers
+from vet.imbue_tools.repo_utils.project_context import ProjectContext
+from vet.imbue_tools.types.vet_config import VetConfig
from vet.issue_identifiers.base import IssueIdentifier
from vet.issue_identifiers.common import GeneratedIssueSchema
from vet.issue_identifiers.common import GeneratedResponseSchema
diff --git a/vet/issue_identifiers/harnesses/base.py b/vet/issue_identifiers/harnesses/base.py
@@ -2,7 +2,7 @@ import abc
from typing import Generic
from typing import TypeVar
-from imbue_tools.get_conversation_history.input_data_types import IdentifierInputs
+from vet.imbue_tools.get_conversation_history.input_data_types import IdentifierInputs
from vet.issue_identifiers.base import IssueIdentifier
from vet.issue_identifiers.identification_guides import (
IssueIdentificationGuide,
diff --git a/vet/issue_identifiers/harnesses/conversation_single_prompt.py b/vet/issue_identifiers/harnesses/conversation_single_prompt.py
@@ -10,21 +10,21 @@ from typing import Generator
import jinja2
-from imbue_core.agents.llm_apis.build_apis import build_language_model_from_config
-from imbue_core.agents.llm_apis.data_types import LanguageModelGenerationParams
-from imbue_core.data_types import AgenticPhase
-from imbue_core.data_types import IssueCode
-from imbue_core.data_types import IssueIdentificationDebugInfo
-from imbue_core.data_types import IssueIdentificationLLMResponseMetadata
-from imbue_core.data_types import LLMResponse
-from imbue_core.itertools import only
-from imbue_tools.get_conversation_history.get_conversation_history import (
+from vet.imbue_core.agents.llm_apis.build_apis import build_language_model_from_config
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelGenerationParams
+from vet.imbue_core.data_types import AgenticPhase
+from vet.imbue_core.data_types import IssueCode
+from vet.imbue_core.data_types import IssueIdentificationDebugInfo
+from vet.imbue_core.data_types import IssueIdentificationLLMResponseMetadata
+from vet.imbue_core.data_types import LLMResponse
+from vet.imbue_core.itertools import only
+from vet.imbue_tools.get_conversation_history.get_conversation_history import (
format_conversation_history_for_prompt,
)
-from imbue_tools.get_conversation_history.input_data_types import ConversationInputs
-from imbue_tools.repo_utils.project_context import ProjectContext
-from imbue_tools.types.vet_config import VetConfig
-from imbue_tools.util_prompts.conversation_prefix import CONVERSATION_PREFIX_TEMPLATE
+from vet.imbue_tools.get_conversation_history.input_data_types import ConversationInputs
+from vet.imbue_tools.repo_utils.project_context import ProjectContext
+from vet.imbue_tools.types.vet_config import VetConfig
+from vet.imbue_tools.util_prompts.conversation_prefix import CONVERSATION_PREFIX_TEMPLATE
from vet.issue_identifiers.base import IssueIdentifier
from vet.issue_identifiers.common import GeneratedIssueSchema
from vet.issue_identifiers.common import GeneratedResponseSchema
diff --git a/vet/issue_identifiers/harnesses/conversation_single_prompt_test.py b/vet/issue_identifiers/harnesses/conversation_single_prompt_test.py
@@ -1,15 +1,15 @@
import pytest
-from imbue_core.data_types import IssueCode
-from vet_types.chat_state import TextBlock
-from vet_types.ids import AssistantMessageID
-from vet_types.messages import AgentMessageSource
-from vet_types.messages import ChatInputUserMessage
-from vet_types.messages import LLMModel
-from vet_types.messages import ResponseBlockAgentMessage
-from imbue_tools.get_conversation_history.input_data_types import ConversationInputs
-from imbue_tools.get_conversation_history.input_data_types import IdentifierInputs
-from imbue_tools.get_conversation_history.input_data_types import (
+from vet.imbue_core.data_types import IssueCode
+from vet.vet_types.chat_state import TextBlock
+from vet.vet_types.ids import AssistantMessageID
+from vet.vet_types.messages import AgentMessageSource
+from vet.vet_types.messages import ChatInputUserMessage
+from vet.vet_types.messages import LLMModel
+from vet.vet_types.messages import ResponseBlockAgentMessage
+from vet.imbue_tools.get_conversation_history.input_data_types import ConversationInputs
+from vet.imbue_tools.get_conversation_history.input_data_types import IdentifierInputs
+from vet.imbue_tools.get_conversation_history.input_data_types import (
IdentifierInputsMissingError,
)
from vet.issue_identifiers.harnesses.conversation_single_prompt import (
diff --git a/vet/issue_identifiers/harnesses/single_prompt.py b/vet/issue_identifiers/harnesses/single_prompt.py
@@ -8,18 +8,18 @@ from typing import Generator
import jinja2
-from imbue_core.agents.llm_apis.build_apis import build_language_model_from_config
-from imbue_core.agents.llm_apis.data_types import LanguageModelGenerationParams
-from imbue_core.data_types import AgenticPhase
-from imbue_core.data_types import IssueCode
-from imbue_core.data_types import IssueIdentificationDebugInfo
-from imbue_core.data_types import IssueIdentificationLLMResponseMetadata
-from imbue_core.data_types import LLMResponse
-from imbue_core.itertools import only
-from imbue_tools.get_conversation_history.input_data_types import CommitInputs
-from imbue_tools.repo_utils.context_utils import escape_prompt_markers
-from imbue_tools.repo_utils.project_context import ProjectContext
-from imbue_tools.types.vet_config import VetConfig
+from vet.imbue_core.agents.llm_apis.build_apis import build_language_model_from_config
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelGenerationParams
+from vet.imbue_core.data_types import AgenticPhase
+from vet.imbue_core.data_types import IssueCode
+from vet.imbue_core.data_types import IssueIdentificationDebugInfo
+from vet.imbue_core.data_types import IssueIdentificationLLMResponseMetadata
+from vet.imbue_core.data_types import LLMResponse
+from vet.imbue_core.itertools import only
+from vet.imbue_tools.get_conversation_history.input_data_types import CommitInputs
+from vet.imbue_tools.repo_utils.context_utils import escape_prompt_markers
+from vet.imbue_tools.repo_utils.project_context import ProjectContext
+from vet.imbue_tools.types.vet_config import VetConfig
from vet.issue_identifiers.base import IssueIdentifier
from vet.issue_identifiers.common import GeneratedIssueSchema
from vet.issue_identifiers.common import GeneratedResponseSchema
diff --git a/vet/issue_identifiers/harnesses/single_prompt_test.py b/vet/issue_identifiers/harnesses/single_prompt_test.py
@@ -7,21 +7,21 @@ from unittest import mock
import pytest
-from imbue_core.agents.llm_apis.data_types import CostedLanguageModelResponse
-from imbue_core.agents.llm_apis.data_types import LanguageModelGenerationParams
-from imbue_core.agents.llm_apis.data_types import LanguageModelResponseUsage
-from imbue_core.agents.llm_apis.data_types import LanguageModelResponseWithLogits
-from imbue_core.agents.llm_apis.data_types import ResponseStopReason
-from imbue_core.agents.llm_apis.mock_api import LanguageModelMock
-from imbue_core.data_types import IssueCode
-from imbue_core.frozen_utils import FrozenDict
-from imbue_tools.get_conversation_history.input_data_types import CommitInputs
-from imbue_tools.get_conversation_history.input_data_types import IdentifierInputs
-from imbue_tools.get_conversation_history.input_data_types import (
+from vet.imbue_core.agents.llm_apis.data_types import CostedLanguageModelResponse
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelGenerationParams
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelResponseUsage
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelResponseWithLogits
+from vet.imbue_core.agents.llm_apis.data_types import ResponseStopReason
+from vet.imbue_core.agents.llm_apis.mock_api import LanguageModelMock
+from vet.imbue_core.data_types import IssueCode
+from vet.imbue_core.frozen_utils import FrozenDict
+from vet.imbue_tools.get_conversation_history.input_data_types import CommitInputs
+from vet.imbue_tools.get_conversation_history.input_data_types import IdentifierInputs
+from vet.imbue_tools.get_conversation_history.input_data_types import (
IdentifierInputsMissingError,
)
-from imbue_tools.repo_utils.project_context import BaseProjectContext
-from imbue_tools.types.vet_config import VetConfig
+from vet.imbue_tools.repo_utils.project_context import BaseProjectContext
+from vet.imbue_tools.types.vet_config import VetConfig
from vet.issue_identifiers.base import IssueIdentifier
from vet.issue_identifiers.harnesses.single_prompt import SinglePromptHarness
from vet.issue_identifiers.identification_guides import (
diff --git a/vet/issue_identifiers/identification_guides.py b/vet/issue_identifiers/identification_guides.py
@@ -1,5 +1,5 @@
-from imbue_core.data_types import IssueCode
-from imbue_core.pydantic_serialization import SerializableModel
+from vet.imbue_core.data_types import IssueCode
+from vet.imbue_core.pydantic_serialization import SerializableModel
class IssueIdentificationGuide(SerializableModel):
diff --git a/vet/issue_identifiers/issue_deduplication.py b/vet/issue_identifiers/issue_deduplication.py
@@ -4,16 +4,16 @@ from typing import Iterable
import jinja2
-from imbue_core.agents.llm_apis.build_apis import build_language_model_from_config
-from imbue_core.agents.llm_apis.data_types import LanguageModelGenerationParams
-from imbue_core.data_types import AgenticPhase
-from imbue_core.data_types import IssueCode
-from imbue_core.data_types import IssueIdentificationDebugInfo
-from imbue_core.data_types import IssueIdentificationLLMResponseMetadata
-from imbue_core.data_types import LLMResponse
-from imbue_core.itertools import only
-from imbue_tools.repo_utils.context_utils import escape_prompt_markers
-from imbue_tools.types.vet_config import VetConfig
+from vet.imbue_core.agents.llm_apis.build_apis import build_language_model_from_config
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelGenerationParams
+from vet.imbue_core.data_types import AgenticPhase
+from vet.imbue_core.data_types import IssueCode
+from vet.imbue_core.data_types import IssueIdentificationDebugInfo
+from vet.imbue_core.data_types import IssueIdentificationLLMResponseMetadata
+from vet.imbue_core.data_types import LLMResponse
+from vet.imbue_core.itertools import only
+from vet.imbue_tools.repo_utils.context_utils import escape_prompt_markers
+from vet.imbue_tools.types.vet_config import VetConfig
from vet.issue_identifiers.common import GeneratedIssueSchema
from vet.issue_identifiers.common import GeneratedResponseSchema
from vet.issue_identifiers.common import (
diff --git a/vet/issue_identifiers/issue_evaluation.py b/vet/issue_identifiers/issue_evaluation.py
@@ -2,30 +2,30 @@ from typing import Generator
import jinja2
-from imbue_core.agents.llm_apis.build_apis import build_language_model_from_config
-from imbue_core.agents.llm_apis.data_types import LanguageModelGenerationParams
-from imbue_core.data_types import AgenticPhase
-from imbue_core.data_types import IssueCode
-from imbue_core.data_types import IssueIdentificationDebugInfo
-from imbue_core.data_types import IssueIdentificationLLMResponseMetadata
-from imbue_core.data_types import LLMResponse
-from imbue_core.itertools import only
-from imbue_core.pydantic_serialization import SerializableModel
-from imbue_tools.get_conversation_history.get_conversation_history import (
+from vet.imbue_core.agents.llm_apis.build_apis import build_language_model_from_config
+from vet.imbue_core.agents.llm_apis.data_types import LanguageModelGenerationParams
+from vet.imbue_core.data_types import AgenticPhase
+from vet.imbue_core.data_types import IssueCode
+from vet.imbue_core.data_types import IssueIdentificationDebugInfo
+from vet.imbue_core.data_types import IssueIdentificationLLMResponseMetadata
+from vet.imbue_core.data_types import LLMResponse
+from vet.imbue_core.itertools import only
+from vet.imbue_core.pydantic_serialization import SerializableModel
+from vet.imbue_tools.get_conversation_history.get_conversation_history import (
format_conversation_history_for_prompt,
)
-from imbue_tools.get_conversation_history.input_data_types import IdentifierInputs
-from imbue_tools.llm_output_parsing.parse_model_json_response import (
+from vet.imbue_tools.get_conversation_history.input_data_types import IdentifierInputs
+from vet.imbue_tools.llm_output_parsing.parse_model_json_response import (
ResponseParsingError,
)
-from imbue_tools.llm_output_parsing.parse_model_json_response import (
+from vet.imbue_tools.llm_output_parsing.parse_model_json_response import (
parse_model_json_response,
)
-from imbue_tools.repo_utils.context_utils import escape_prompt_markers
-from imbue_tools.repo_utils.project_context import ProjectContext
-from imbue_tools.types.vet_config import DEFAULT_CONFIDENCE_THRESHOLD
-from imbue_tools.types.vet_config import VetConfig
-from imbue_tools.util_prompts.conversation_prefix import CONVERSATION_PREFIX_TEMPLATE
+from vet.imbue_tools.repo_utils.context_utils import escape_prompt_markers
+from vet.imbue_tools.repo_utils.project_context import ProjectContext
+from vet.imbue_tools.types.vet_config import DEFAULT_CONFIDENCE_THRESHOLD
+from vet.imbue_tools.types.vet_config import VetConfig
+from vet.imbue_tools.util_prompts.conversation_prefix import CONVERSATION_PREFIX_TEMPLATE
from vet.issue_identifiers.common import GeneratedIssueSchema
from vet.issue_identifiers.common import (
extract_invocation_info_from_costed_response,
diff --git a/vet/issue_identifiers/registry.py b/vet/issue_identifiers/registry.py
@@ -11,19 +11,19 @@ from typing import TypeVar
from loguru import logger
-from imbue_core.agents.primitives.resource_limits import ensure_global_resource_limits
-from imbue_core.data_types import IssueCode
-from imbue_core.data_types import IssueIdentificationDebugInfo
-from imbue_core.data_types import IssueIdentificationLLMResponseMetadata
-from imbue_core.data_types import IssueIdentifierResult
-from imbue_core.data_types import IssueIdentifierType
-from imbue_tools.get_conversation_history.input_data_types import IdentifierInputs
-from imbue_tools.get_conversation_history.input_data_types import (
+from vet.imbue_core.agents.primitives.resource_limits import ensure_global_resource_limits
+from vet.imbue_core.data_types import IssueCode
+from vet.imbue_core.data_types import IssueIdentificationDebugInfo
+from vet.imbue_core.data_types import IssueIdentificationLLMResponseMetadata
+from vet.imbue_core.data_types import IssueIdentifierResult
+from vet.imbue_core.data_types import IssueIdentifierType
+from vet.imbue_tools.get_conversation_history.input_data_types import IdentifierInputs
+from vet.imbue_tools.get_conversation_history.input_data_types import (
IdentifierInputsMissingError,
)
-from imbue_tools.repo_utils.project_context import ProjectContext
-from imbue_tools.types.vet_config import VetConfig
-from imbue_tools.types.vet_config import get_enabled_issue_codes
+from vet.imbue_tools.repo_utils.project_context import ProjectContext
+from vet.imbue_tools.types.vet_config import VetConfig
+from vet.imbue_tools.types.vet_config import get_enabled_issue_codes
from vet.issue_identifiers.agentic_issue_collation import (
collate_issues_with_agent,
)
diff --git a/vet/issue_identifiers/test_prompt_lengths.py b/vet/issue_identifiers/test_prompt_lengths.py
@@ -1,9 +1,9 @@
-from imbue_core.data_types import IssueIdentifierType
-from imbue_core.frozen_utils import FrozenDict
-from imbue_core.itertools import first
-from imbue_tools.get_conversation_history.input_data_types import CommitInputs
-from imbue_tools.repo_utils.project_context import BaseProjectContext
-from imbue_tools.types.vet_config import VetConfig
+from vet.imbue_core.data_types import IssueIdentifierType
+from vet.imbue_core.frozen_utils import FrozenDict
+from vet.imbue_core.itertools import first
+from vet.imbue_tools.get_conversation_history.input_data_types import CommitInputs
+from vet.imbue_tools.repo_utils.project_context import BaseProjectContext
+from vet.imbue_tools.types.vet_config import VetConfig
from vet.issue_identifiers import registry
from vet.issue_identifiers.identification_guides import (
ISSUE_IDENTIFICATION_GUIDES_BY_ISSUE_CODE,
diff --git a/vet/repo_utils.py b/vet/repo_utils.py
@@ -1,6 +1,6 @@
from pathlib import Path
-from imbue_core.async_monkey_patches import log_exception
+from vet.imbue_core.async_monkey_patches import log_exception
from vet.errors import GitException
from vet.errors import RunCommandError
from vet.git import SyncLocalGitRepo
diff --git a/vet/repo_utils_test.py b/vet/repo_utils_test.py
@@ -3,11 +3,11 @@ from pathlib import Path
from syrupy.assertion import SnapshotAssertion
-from imbue_core.agents.llm_apis.anthropic_api import AnthropicModelName
-from imbue_core.nested_evolver import assign
-from imbue_core.nested_evolver import chill
-from imbue_core.nested_evolver import evolver
-from imbue_tools.repo_utils.project_context import LazyProjectContext
+from vet.imbue_core.agents.llm_apis.anthropic_api import AnthropicModelName
+from vet.imbue_core.nested_evolver import assign
+from vet.imbue_core.nested_evolver import chill
+from vet.imbue_core.nested_evolver import evolver
+from vet.imbue_tools.repo_utils.project_context import LazyProjectContext
from vet.repo_utils import get_code_to_check
diff --git a/vet/vet_types/__init__.py b/vet/vet_types/__init__.py
@@ -0,0 +1,33 @@
+"""Shared type definitions for Vet."""
+
+from vet.vet_types.chat_state import ContentBlock
+from vet.vet_types.chat_state import ContentBlockTypes
+from vet.vet_types.chat_state import TextBlock
+from vet.vet_types.chat_state import ToolResultBlock
+from vet.vet_types.chat_state import ToolUseBlock
+from vet.vet_types.ids import AgentMessageID
+from vet.vet_types.ids import AssistantMessageID
+from vet.vet_types.ids import TaskID
+from vet.vet_types.ids import ToolUseID
+from vet.vet_types.messages import AgentMessageSource
+from vet.vet_types.messages import ChatInputUserMessage
+from vet.vet_types.messages import ConversationMessageUnion
+from vet.vet_types.messages import LLMModel
+from vet.vet_types.messages import ResponseBlockAgentMessage
+
+__all__ = [
+ "AgentMessageID",
+ "AgentMessageSource",
+ "AssistantMessageID",
+ "ChatInputUserMessage",
+ "ContentBlock",
+ "ContentBlockTypes",
+ "ConversationMessageUnion",
+ "LLMModel",
+ "ResponseBlockAgentMessage",
+ "TaskID",
+ "TextBlock",
+ "ToolResultBlock",
+ "ToolUseBlock",
+ "ToolUseID",
+]
diff --git a/vet/vet_types/chat_state.py b/vet/vet_types/chat_state.py
@@ -0,0 +1,153 @@
+"""Chat state types for Vet."""
+
+from typing import Annotated
+from typing import Any
+from typing import Literal
+
+from pydantic import Field
+from pydantic import Tag
+
+from vet.imbue_core.pydantic_serialization import SerializableModel
+from vet.imbue_core.pydantic_serialization import build_discriminator
+from vet.vet_types.ids import TaskID
+from vet.vet_types.ids import ToolUseID
+
+
+# ========================
+# Chat Type Definitions
+# ========================
+
+
+class ContentBlock(SerializableModel):
+ object_type: str = Field(..., description="Type discriminator for content blocks")
+ type: str = Field(..., description="Type discriminator for content blocks")
+
+
+class TextBlock(ContentBlock):
+ object_type: str = "TextBlock"
+ type: Literal["text"] = "text"
+ text: str
+
+
+class ContextSummaryBlock(ContentBlock):
+ object_type: str = "ContextSummaryBlock"
+ type: Literal["context_summary"] = "context_summary"
+ text: str
+
+
+class ResumeResponseBlock(ContentBlock):
+ object_type: str = "ResumeResponseBlock"
+ type: Literal["resume_response"] = "resume_response"
+
+
+class ForkedToBlock(ContentBlock):
+ object_type: str = "ForkedToBlock"
+ type: Literal["forked_to"] = "forked_to"
+ forked_to_task_id: TaskID
+
+
+class ForkedFromBlock(ContentBlock):
+ object_type: str = "ForkedFromBlock"
+ type: Literal["forked_from"] = "forked_from"
+ forked_from_task_id: TaskID
+
+
+class CommandBlock(ContentBlock):
+ object_type: str = "CommandBlock"
+ type: Literal["command"] = "command"
+ command: str
+ is_automated: bool = Field(default=False, description="Whether the command is automated")
+
+
+ToolInput = dict[str, Any]
+
+
+class ToolUseBlock(ContentBlock):
+ object_type: str = "ToolUseBlock"
+ type: Literal["tool_use"] = "tool_use"
+ id: ToolUseID = Field(..., description="Unique identifier for this tool use")
+ name: str = Field(..., description="Name of the tool being used")
+ input: ToolInput = Field(default_factory=ToolInput, description="Input parameters for the tool")
+
+
+class ToolResultContent(SerializableModel):
+ """Base class for tool result content with type discriminator"""
+
+ content_type: str = Field(..., description="Type discriminator for tool result content")
+
+
+class SimpleToolContent(ToolResultContent):
+ """Generic tool content, or information to reconstruct diff tool content"""
+
+ content_type: Literal["simple"] = "simple"
+ text: str = Field(..., description="The tool output as text")
+ tool_input: ToolInput
+ tool_content: Any
+
+
+class GenericToolContent(ToolResultContent):
+ """Generic content for most tools - just a string"""
+
+ content_type: Literal["generic"] = "generic"
+ text: str = Field(..., description="The tool output as text")
+
+
+class DiffToolContent(ToolResultContent):
+ """Content for diff-producing tools (Write, Edit, MultiEdit)"""
+
+ content_type: Literal["diff"] = "diff"
+ diff: str = Field(..., description="The git diff string")
+ file_path: str = Field(..., description="The file that was modified")
+
+
+ToolResultContentType = GenericToolContent | DiffToolContent
+
+
+class ToolResultBlock(ContentBlock):
+ object_type: str = "ToolResultBlock"
+ type: Literal["tool_result"] = "tool_result"
+ tool_use_id: ToolUseID = Field(..., description="ID of the corresponding tool use")
+ tool_name: str = Field(..., description="Name of the tool that was used")
+ invocation_string: str = Field(..., description="String representation of how the tool was invoked")
+ content: ToolResultContentType = Field(..., description="Result content from the tool execution")
+ is_error: bool = Field(default=False, description="Whether the tool execution resulted in an error")
+
+
+class WarningBlock(ContentBlock):
+ object_type: str = "WarningBlock"
+ type: Literal["warning"] = "warning"
+ message: str = Field(..., description="Warning message")
+ traceback: str | None = Field(..., description="Warning traceback")
+ warning_type: str | None = Field(..., description="Type of warning, i.e. name of the exception that was raised")
+
+
+class ErrorBlock(ContentBlock):
+ object_type: str = "ErrorBlock"
+ type: Literal["error"] = "error"
+ message: str = Field(..., description="Error message")
+ traceback: str = Field(..., description="Error traceback")
+ error_type: str = Field(..., description="Type of error, i.e. name of the exception that was raised")
+
+
+class FileBlock(ContentBlock):
+ object_type: str = "FileBlock"
+ type: Literal["file"] = "file"
+ source: str = Field(..., description="A file path on the users local machine.")
+
+
+ContentBlockTypes = Annotated[
+ (
+ Annotated[TextBlock, Tag("TextBlock")]
+ | Annotated[CommandBlock, Tag("CommandBlock")]
+ | Annotated[ToolUseBlock, Tag("ToolUseBlock")]
+ | Annotated[ToolResultBlock, Tag("ToolResultBlock")]
+ | Annotated[ErrorBlock, Tag("ErrorBlock")]
+ | Annotated[WarningBlock, Tag("WarningBlock")]
+ | Annotated[ContextSummaryBlock, Tag("ContextSummaryBlock")]
+ | Annotated[ResumeResponseBlock, Tag("ResumeResponseBlock")]
+ | Annotated[FileBlock, Tag("FileBlock")]
+ | Annotated[ForkedToBlock, Tag("ForkedToBlock")]
+ | Annotated[ForkedFromBlock, Tag("ForkedFromBlock")]
+ ),
+ build_discriminator(),
+]
diff --git a/vet_types/vet_types/ids.py b/vet/vet_types/ids.py
diff --git a/vet/vet_types/messages.py b/vet/vet_types/messages.py
@@ -0,0 +1,130 @@
+"""Message types for Vet conversation history.
+
+These are simplified versions that avoid dependencies on external telemetry libraries.
+"""
+
+import datetime
+from enum import StrEnum
+from typing import Annotated
+from typing import Literal
+
+from pydantic import Field
+from pydantic import Tag
+
+from vet.imbue_core.pydantic_serialization import SerializableModel
+from vet.imbue_core.pydantic_serialization import build_discriminator
+from vet.imbue_core.time_utils import get_current_time
+from vet.vet_types.chat_state import ContentBlockTypes
+from vet.vet_types.ids import AgentMessageID
+from vet.vet_types.ids import AssistantMessageID
+
+
+class LLMModel(StrEnum):
+ CLAUDE_4_OPUS = "CLAUDE-4-OPUS"
+ CLAUDE_4_SONNET = "CLAUDE-4-SONNET"
+ CLAUDE_4_HAIKU = "CLAUDE-4-HAIKU"
+ GPT_5_1_CODEX = "GPT-5.1-CODEX"
+ GPT_5_1_CODEX_MINI = "GPT-5.1-CODEX-MINI"
+ GPT_5_1 = "GPT-5.1"
+ GPT_5_2 = "GPT-5.2"
+
+
+# ==================================
+# Backend Message Type Definitions
+# ==================================
+
+
+class AgentMessageSource(StrEnum):
+ """
+ Messages can come from the AGENT (LLM), USER (chat messages & direct interactions),
+ SYSTEM (app and service code) and RUNNER (the process controlling a task).
+ """
+
+ # Messages coming directly from the agent.
+ AGENT = "AGENT"
+
+ # Messages coming directly from a user interacting with the interface, ie chat.
+ USER = "USER"
+
+ # Messages coming from system-mediated actions and automations.
+ SYSTEM = "SYSTEM"
+
+ # Messages coming from the task runner wrapper, such as environment shutdown.
+ RUNNER = "RUNNER"
+
+
+class Message(SerializableModel):
+ """Base class for all messages sent to or from the agent and user."""
+
+ # used to dispatch and discover the type of message
+ object_type: str
+ # the unique ID of the message, used to track it across the system and prevent duplicates.
+ message_id: AgentMessageID = Field(default_factory=AgentMessageID)
+ # the source of the message, which can be either the agent, user, or runner.
+ source: AgentMessageSource
+ # roughly when the message was created, in UTC.
+ approximate_creation_time: datetime.datetime = Field(default_factory=get_current_time)
+
+ @property
+ def is_ephemeral(self) -> bool:
+ raise NotImplementedError("All messages must be subclassed off of PersistentMessage or EphemeralMessage")
+
+
+class PersistentMessage(Message):
+ @property
+ def is_ephemeral(self) -> bool:
+ return False
+
+
+class PersistentUserMessage(PersistentMessage):
+ """
+ One of two base classes for messages sent from the user.
+ Persistent user messages are saved to the database.
+ """
+
+ object_type: str = Field(
+ default="PersistentUserMessage",
+ description="Type discriminator for user messages",
+ )
+ message_id: AgentMessageID = Field(
+ default_factory=AgentMessageID,
+ description="Unique identifier for the user message",
+ )
+ source: AgentMessageSource = Field(default=AgentMessageSource.USER)
+ approximate_creation_time: datetime.datetime = Field(
+ default_factory=get_current_time,
+ description="Approximate UTC timestamp when user message was created",
+ )
+
+
+class ChatInputUserMessage(PersistentUserMessage):
+ object_type: str = Field(default="ChatInputUserMessage")
+ text: str = Field(..., description="User input text content")
+ model_name: LLMModel | None = Field(
+ default=None,
+ description="Selected LLM model for the chat request",
+ )
+ files: list[str] = Field(
+ default_factory=list,
+ description="List of file paths attached to this message",
+ )
+
+
+class PersistentAgentMessage(PersistentMessage):
+ """Base class for messages sent from the agent."""
+
+ source: AgentMessageSource = AgentMessageSource.AGENT
+
+
+class ResponseBlockAgentMessage(PersistentAgentMessage):
+ object_type: str = "ResponseBlockAgentMessage"
+ role: Literal["user", "assistant", "system"]
+ assistant_message_id: AssistantMessageID
+ content: tuple[ContentBlockTypes, ...]
+
+
+ConversationMessageUnion = Annotated[
+ Annotated[ResponseBlockAgentMessage, Tag("ResponseBlockAgentMessage")]
+ | Annotated[ChatInputUserMessage, Tag("ChatInputUserMessage")],
+ build_discriminator(),
+]
diff --git a/vet_types/pyproject.toml b/vet_types/pyproject.toml
@@ -1,20 +0,0 @@
-[build-system]
-requires = ["setuptools", "wheel"]
-build-backend = "setuptools.build_meta"
-
-[project]
-name = "vet_types"
-version = "0.1.0"
-description = "Type definitions for Vet without telemetry dependencies"
-dependencies = [
- "pydantic",
- "imbue_core",
- "typeid-python",
-]
-requires-python = ">=3.11"
-
-[tool.setuptools]
-package-data.vet_types = ["py.typed"]
-
-[tool.setuptools.packages.find]
-include = ["vet_types*"]
diff --git a/vet_types/vet_types/__init__.py b/vet_types/vet_types/__init__.py
@@ -1,33 +0,0 @@
-"""Shared type definitions for Vet."""
-
-from vet_types.chat_state import ContentBlock
-from vet_types.chat_state import ContentBlockTypes
-from vet_types.chat_state import TextBlock
-from vet_types.chat_state import ToolResultBlock
-from vet_types.chat_state import ToolUseBlock
-from vet_types.ids import AgentMessageID
-from vet_types.ids import AssistantMessageID
-from vet_types.ids import TaskID
-from vet_types.ids import ToolUseID
-from vet_types.messages import AgentMessageSource
-from vet_types.messages import ChatInputUserMessage
-from vet_types.messages import ConversationMessageUnion
-from vet_types.messages import LLMModel
-from vet_types.messages import ResponseBlockAgentMessage
-
-__all__ = [
- "AgentMessageID",
- "AgentMessageSource",
- "AssistantMessageID",
- "ChatInputUserMessage",
- "ContentBlock",
- "ContentBlockTypes",
- "ConversationMessageUnion",
- "LLMModel",
- "ResponseBlockAgentMessage",
- "TaskID",
- "TextBlock",
- "ToolResultBlock",
- "ToolUseBlock",
- "ToolUseID",
-]
diff --git a/vet_types/vet_types/chat_state.py b/vet_types/vet_types/chat_state.py
@@ -1,153 +0,0 @@
-"""Chat state types for Vet."""
-
-from typing import Annotated
-from typing import Any
-from typing import Literal
-
-from pydantic import Field
-from pydantic import Tag
-
-from imbue_core.pydantic_serialization import SerializableModel
-from imbue_core.pydantic_serialization import build_discriminator
-from vet_types.ids import TaskID
-from vet_types.ids import ToolUseID
-
-
-# ========================
-# Chat Type Definitions
-# ========================
-
-
-class ContentBlock(SerializableModel):
- object_type: str = Field(..., description="Type discriminator for content blocks")
- type: str = Field(..., description="Type discriminator for content blocks")
-
-
-class TextBlock(ContentBlock):
- object_type: str = "TextBlock"
- type: Literal["text"] = "text"
- text: str
-
-
-class ContextSummaryBlock(ContentBlock):
- object_type: str = "ContextSummaryBlock"
- type: Literal["context_summary"] = "context_summary"
- text: str
-
-
-class ResumeResponseBlock(ContentBlock):
- object_type: str = "ResumeResponseBlock"
- type: Literal["resume_response"] = "resume_response"
-
-
-class ForkedToBlock(ContentBlock):
- object_type: str = "ForkedToBlock"
- type: Literal["forked_to"] = "forked_to"
- forked_to_task_id: TaskID
-
-
-class ForkedFromBlock(ContentBlock):
- object_type: str = "ForkedFromBlock"
- type: Literal["forked_from"] = "forked_from"
- forked_from_task_id: TaskID
-
-
-class CommandBlock(ContentBlock):
- object_type: str = "CommandBlock"
- type: Literal["command"] = "command"
- command: str
- is_automated: bool = Field(default=False, description="Whether the command is automated")
-
-
-ToolInput = dict[str, Any]
-
-
-class ToolUseBlock(ContentBlock):
- object_type: str = "ToolUseBlock"
- type: Literal["tool_use"] = "tool_use"
- id: ToolUseID = Field(..., description="Unique identifier for this tool use")
- name: str = Field(..., description="Name of the tool being used")
- input: ToolInput = Field(default_factory=ToolInput, description="Input parameters for the tool")
-
-
-class ToolResultContent(SerializableModel):
- """Base class for tool result content with type discriminator"""
-
- content_type: str = Field(..., description="Type discriminator for tool result content")
-
-
-class SimpleToolContent(ToolResultContent):
- """Generic tool content, or information to reconstruct diff tool content"""
-
- content_type: Literal["simple"] = "simple"
- text: str = Field(..., description="The tool output as text")
- tool_input: ToolInput
- tool_content: Any
-
-
-class GenericToolContent(ToolResultContent):
- """Generic content for most tools - just a string"""
-
- content_type: Literal["generic"] = "generic"
- text: str = Field(..., description="The tool output as text")
-
-
-class DiffToolContent(ToolResultContent):
- """Content for diff-producing tools (Write, Edit, MultiEdit)"""
-
- content_type: Literal["diff"] = "diff"
- diff: str = Field(..., description="The git diff string")
- file_path: str = Field(..., description="The file that was modified")
-
-
-ToolResultContentType = GenericToolContent | DiffToolContent
-
-
-class ToolResultBlock(ContentBlock):
- object_type: str = "ToolResultBlock"
- type: Literal["tool_result"] = "tool_result"
- tool_use_id: ToolUseID = Field(..., description="ID of the corresponding tool use")
- tool_name: str = Field(..., description="Name of the tool that was used")
- invocation_string: str = Field(..., description="String representation of how the tool was invoked")
- content: ToolResultContentType = Field(..., description="Result content from the tool execution")
- is_error: bool = Field(default=False, description="Whether the tool execution resulted in an error")
-
-
-class WarningBlock(ContentBlock):
- object_type: str = "WarningBlock"
- type: Literal["warning"] = "warning"
- message: str = Field(..., description="Warning message")
- traceback: str | None = Field(..., description="Warning traceback")
- warning_type: str | None = Field(..., description="Type of warning, i.e. name of the exception that was raised")
-
-
-class ErrorBlock(ContentBlock):
- object_type: str = "ErrorBlock"
- type: Literal["error"] = "error"
- message: str = Field(..., description="Error message")
- traceback: str = Field(..., description="Error traceback")
- error_type: str = Field(..., description="Type of error, i.e. name of the exception that was raised")
-
-
-class FileBlock(ContentBlock):
- object_type: str = "FileBlock"
- type: Literal["file"] = "file"
- source: str = Field(..., description="A file path on the users local machine.")
-
-
-ContentBlockTypes = Annotated[
- (
- Annotated[TextBlock, Tag("TextBlock")]
- | Annotated[CommandBlock, Tag("CommandBlock")]
- | Annotated[ToolUseBlock, Tag("ToolUseBlock")]
- | Annotated[ToolResultBlock, Tag("ToolResultBlock")]
- | Annotated[ErrorBlock, Tag("ErrorBlock")]
- | Annotated[WarningBlock, Tag("WarningBlock")]
- | Annotated[ContextSummaryBlock, Tag("ContextSummaryBlock")]
- | Annotated[ResumeResponseBlock, Tag("ResumeResponseBlock")]
- | Annotated[FileBlock, Tag("FileBlock")]
- | Annotated[ForkedToBlock, Tag("ForkedToBlock")]
- | Annotated[ForkedFromBlock, Tag("ForkedFromBlock")]
- ),
- build_discriminator(),
-]
diff --git a/vet_types/vet_types/messages.py b/vet_types/vet_types/messages.py
@@ -1,130 +0,0 @@
-"""Message types for Vet conversation history.
-
-These are simplified versions that avoid dependencies on external telemetry libraries.
-"""
-
-import datetime
-from enum import StrEnum
-from typing import Annotated
-from typing import Literal
-
-from pydantic import Field
-from pydantic import Tag
-
-from imbue_core.pydantic_serialization import SerializableModel
-from imbue_core.pydantic_serialization import build_discriminator
-from imbue_core.time_utils import get_current_time
-from vet_types.chat_state import ContentBlockTypes
-from vet_types.ids import AgentMessageID
-from vet_types.ids import AssistantMessageID
-
-
-class LLMModel(StrEnum):
- CLAUDE_4_OPUS = "CLAUDE-4-OPUS"
- CLAUDE_4_SONNET = "CLAUDE-4-SONNET"
- CLAUDE_4_HAIKU = "CLAUDE-4-HAIKU"
- GPT_5_1_CODEX = "GPT-5.1-CODEX"
- GPT_5_1_CODEX_MINI = "GPT-5.1-CODEX-MINI"
- GPT_5_1 = "GPT-5.1"
- GPT_5_2 = "GPT-5.2"
-
-
-# ==================================
-# Backend Message Type Definitions
-# ==================================
-
-
-class AgentMessageSource(StrEnum):
- """
- Messages can come from the AGENT (LLM), USER (chat messages & direct interactions),
- SYSTEM (app and service code) and RUNNER (the process controlling a task).
- """
-
- # Messages coming directly from the agent.
- AGENT = "AGENT"
-
- # Messages coming directly from a user interacting with the interface, ie chat.
- USER = "USER"
-
- # Messages coming from system-mediated actions and automations.
- SYSTEM = "SYSTEM"
-
- # Messages coming from the task runner wrapper, such as environment shutdown.
- RUNNER = "RUNNER"
-
-
-class Message(SerializableModel):
- """Base class for all messages sent to or from the agent and user."""
-
- # used to dispatch and discover the type of message
- object_type: str
- # the unique ID of the message, used to track it across the system and prevent duplicates.
- message_id: AgentMessageID = Field(default_factory=AgentMessageID)
- # the source of the message, which can be either the agent, user, or runner.
- source: AgentMessageSource
- # roughly when the message was created, in UTC.
- approximate_creation_time: datetime.datetime = Field(default_factory=get_current_time)
-
- @property
- def is_ephemeral(self) -> bool:
- raise NotImplementedError("All messages must be subclassed off of PersistentMessage or EphemeralMessage")
-
-
-class PersistentMessage(Message):
- @property
- def is_ephemeral(self) -> bool:
- return False
-
-
-class PersistentUserMessage(PersistentMessage):
- """
- One of two base classes for messages sent from the user.
- Persistent user messages are saved to the database.
- """
-
- object_type: str = Field(
- default="PersistentUserMessage",
- description="Type discriminator for user messages",
- )
- message_id: AgentMessageID = Field(
- default_factory=AgentMessageID,
- description="Unique identifier for the user message",
- )
- source: AgentMessageSource = Field(default=AgentMessageSource.USER)
- approximate_creation_time: datetime.datetime = Field(
- default_factory=get_current_time,
- description="Approximate UTC timestamp when user message was created",
- )
-
-
-class ChatInputUserMessage(PersistentUserMessage):
- object_type: str = Field(default="ChatInputUserMessage")
- text: str = Field(..., description="User input text content")
- model_name: LLMModel | None = Field(
- default=None,
- description="Selected LLM model for the chat request",
- )
- files: list[str] = Field(
- default_factory=list,
- description="List of file paths attached to this message",
- )
-
-
-class PersistentAgentMessage(PersistentMessage):
- """Base class for messages sent from the agent."""
-
- source: AgentMessageSource = AgentMessageSource.AGENT
-
-
-class ResponseBlockAgentMessage(PersistentAgentMessage):
- object_type: str = "ResponseBlockAgentMessage"
- role: Literal["user", "assistant", "system"]
- assistant_message_id: AssistantMessageID
- content: tuple[ContentBlockTypes, ...]
-
-
-ConversationMessageUnion = Annotated[
- Annotated[ResponseBlockAgentMessage, Tag("ResponseBlockAgentMessage")]
- | Annotated[ChatInputUserMessage, Tag("ChatInputUserMessage")],
- build_discriminator(),
-]