From e18827614c7cd94097a73504e3966d521457539f Mon Sep 17 00:00:00 2001 From: QueryPlanner Date: Thu, 22 Oct 2026 09:09:57 +0530 Subject: [PATCH] feat: replace litellm with openrouter adapter - Add a thin ADK OpenRouter adapter over the OpenAI SDK - Remove LiteLLM from runtime and dev dependencies - Map ADK messages, tools, images, reasoning, and usage - Record provider-reported OpenRouter costs in the ledger - Switch mem0 to OpenRouter's OpenAI-compatible provider - Update docs and tests for the new model boundary --- .env.example | 11 +- AGENTS.md | 4 +- docs/architecture.md | 6 +- docs/base-infra/environment-variables.md | 5 +- docs/base-infra/observability.md | 5 +- pyproject.toml | 5 +- src/blacki/dashboard/static/dashboard.js | 4 +- src/blacki/memory/config.py | 32 +- src/blacki/models/capabilities.py | 27 +- src/blacki/models/factory.py | 68 +- src/blacki/models/inference.py | 10 +- src/blacki/models/openrouter.py | 162 ++++ src/blacki/models/openrouter_ids.py | 35 + src/blacki/models/openrouter_request.py | 564 ++++++++++++ src/blacki/models/openrouter_response.py | 500 +++++++++++ src/blacki/observability/costs.py | 63 +- src/blacki/server.py | 2 +- src/blacki/tools/brave_search.py | 4 +- src/blacki/utils/config.py | 2 +- tests/memory/test_config.py | 68 +- tests/test_deployment_contract.py | 17 + tests/test_integration.py | 10 +- tests/test_llm_costs.py | 126 ++- tests/test_model_capabilities.py | 2 + tests/test_model_factory.py | 70 +- tests/test_openrouter_ids.py | 47 + tests/test_openrouter_llm.py | 452 ++++++++++ tests/test_openrouter_request.py | 652 ++++++++++++++ tests/test_openrouter_response.py | 738 +++++++++++++++ uv.lock | 1039 +--------------------- 30 files changed, 3473 insertions(+), 1257 deletions(-) create mode 100644 src/blacki/models/openrouter.py create mode 100644 src/blacki/models/openrouter_ids.py create mode 100644 src/blacki/models/openrouter_request.py create mode 100644 src/blacki/models/openrouter_response.py create mode 100644 tests/test_openrouter_ids.py create mode 100644 tests/test_openrouter_llm.py create mode 100644 tests/test_openrouter_request.py create mode 100644 tests/test_openrouter_response.py diff --git a/.env.example b/.env.example index d4de40d..2f37e2f 100644 --- a/.env.example +++ b/.env.example @@ -160,17 +160,18 @@ TELEGRAM_ENABLED=false # intentionally switching to Mem0 Platform in custom code. # # Production setup: -# - OPENROUTER_API_KEY powers Mem0's LLM extraction via LiteLLM. +# - OPENROUTER_API_KEY powers Mem0's LLM extraction through OpenRouter's +# OpenAI-compatible API. # - GOOGLE_API_KEY powers Gemini embedding generation. # - Qdrant Cloud stores and searches the generated vectors. # # Qdrant does not calculate embeddings in this Mem0 flow. If you change the # embedding model or dimensions, create a new collection and re-embed memories. # -# LLM Configuration (uses OPENROUTER_API_KEY by default through LiteLLM) -# MEM0_LLM_PROVIDER=litellm -# MEM0_LLM_MODEL=openrouter/google/gemini-2.0-flash-001 -# MEM0_LLM_API_KEY= # Optional override; defaults to OPENROUTER_API_KEY +# LLM Configuration (uses OPENROUTER_API_KEY by default through OpenRouter) +# MEM0_LLM_PROVIDER=openai +# MEM0_LLM_MODEL=google/gemini-2.0-flash-001 +# MEM0_LLM_API_KEY= # Optional; mem0's OpenAI client prefers OPENROUTER_API_KEY # MEM0_LLM_TEMPERATURE=0.1 # MEM0_LLM_MAX_TOKENS=1000 # diff --git a/AGENTS.md b/AGENTS.md index aff64e7..ec50183 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -122,7 +122,7 @@ observable foundation that can run on bare metal, a VPS, or a private cloud. ### Key Technologies * **Language:** Python 3.13+ * **Framework:** Google ADK (`google-adk`) -* **Model Interface:** LiteLLM (supports Google, OpenRouter, etc.) +* **Model Interface:** OpenRouter (OpenAI-compatible HTTP) with native Google ADK Gemini fallback * **Server:** FastAPI * **Database:** PostgreSQL (via `asyncpg`) * **Observability:** OpenTelemetry (OTel) with vendor-neutral OTLP configuration @@ -219,7 +219,7 @@ When asked to write or modify tests, you **MUST** adhere to the following strict 1. **Real Code Over Mocks**: * **Do not mock** internal logic (e.g., `LlmAgent`, `Prompt`, `Tool`). Use the real classes. - * **Only mock** external boundaries (e.g., `LiteLLM`, `asyncpg`, `Network APIs`). + * **Only mock** external boundaries (e.g., `OpenAI SDK`, `asyncpg`, `Network APIs`). * **Why?** This ensures we test the integration of components, not just isolated units. 2. **Pytest Best Practices**: diff --git a/docs/architecture.md b/docs/architecture.md index 754b585..2b56c5d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -16,12 +16,12 @@ python -m blacki.server Telegram long polling, and exposes `/live`, `/ready`, and `/health`. It also mounts the package-backed private observability dashboard at `/dashboard` with APIs for aggregate statistics, users, sessions, local logs, -and local traces. LiteLLM-backed requests additionally write content-free +and local traces. OpenRouter-backed requests additionally write content-free usage and cost records to the local SQLite ledger so the dashboard can show per-user and per-session spend without parsing prompts or responses. -`src/blacki/agent.py` creates the `LlmAgent`, selects a native Gemini model or a -LiteLLM/OpenRouter model from the environment, registers tools, and assembles +`src/blacki/agent.py` creates the `LlmAgent`, selects a native Gemini model or an +OpenRouter-routed model from the environment, registers tools, and assembles the ADK application plugins. ## Application composition diff --git a/docs/base-infra/environment-variables.md b/docs/base-infra/environment-variables.md index 20125c7..fba6c3d 100644 --- a/docs/base-infra/environment-variables.md +++ b/docs/base-infra/environment-variables.md @@ -69,8 +69,9 @@ ALLOW_ORIGINS=["http://127.0.0.1","http://127.0.0.1:8080"] OPENROUTER_API_KEY=replace-me ``` - Blacki builds a LiteLLM model and normalizes common model identifiers to - OpenRouter form when this key is present. + Blacki builds an OpenRouter adapter and normalizes common model + identifiers to OpenRouter form when this key is present. Both + `openrouter/author/slug` and `author/slug` are accepted. Set `ROOT_AGENT_REASONING_EFFORT` to configure the process-wide reasoning fallback. Supported values are `max`, `xhigh`, `high`, `medium`, `low`, diff --git a/docs/base-infra/observability.md b/docs/base-infra/observability.md index f5293a1..adbf10d 100644 --- a/docs/base-infra/observability.md +++ b/docs/base-infra/observability.md @@ -120,12 +120,13 @@ Telegram `/reset` starts the next versioned ADK session and leaves earlier session rows available. The append-only log and trace files are not deleted by `/reset`, so the dashboard continues to show that history. -For LiteLLM-backed requests, Blacki also writes a content-free SQLite usage +For OpenRouter-backed requests, Blacki also writes a content-free SQLite usage ledger at `{AGENT_DIR}/.adk/costs.db` (override with `BLACKI_COST_LEDGER_PATH`). The ledger stores identity, model, token, provider response, and fixed-point cost fields, but never prompts, responses, or tool arguments. Provider-reported OpenRouter account cost and upstream inference -cost are kept separately; a LiteLLM catalog calculation is labelled estimated. +cost are kept separately; historical gateway estimates remain labelled +estimated, while new OpenRouter rows use provider-reported costs only. The dashboard uses UTC calendar months for monthly totals and averages. The average is across users with an exact or estimated cost in the current month; users with unavailable cost are excluded and the reported coverage is shown. diff --git a/pyproject.toml b/pyproject.toml index 652ca63..7d5d6f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ dependencies = [ "opentelemetry-instrumentation-logging>=0.63b0,<0.64", "pydantic>=2.11.0,<3.0.0", "python-dotenv>=1.0.0,<2.0.0", - "litellm>=1.60.0", + "openai>=2.20,<3", "aiosqlite>=0.20.0", "greenlet>=3.0.0", "openinference-instrumentation-google-adk==0.1.17", @@ -40,7 +40,8 @@ server = "blacki.server:main" [dependency-groups] dev = [ - "google-adk[eval]==2.5.0", + # `google-adk[eval]` is intentionally omitted: it pulls LiteLLM through + # google-cloud-aiplatform[evaluation]. Install it explicitly for `adk eval`. "ruff>=0.12.7,<1.0.0", "mypy>=1.17.1,<2.0.0", "pytest>=8.3.4,<9.0.0", diff --git a/src/blacki/dashboard/static/dashboard.js b/src/blacki/dashboard/static/dashboard.js index 8d24750..04ecd02 100644 --- a/src/blacki/dashboard/static/dashboard.js +++ b/src/blacki/dashboard/static/dashboard.js @@ -219,9 +219,9 @@ setText("metric-cost-coverage", formatPercent(costCoverage)); const costKind = first(metrics, ["cost_kind"], first(cost, ["kind"])); if (costKind === "estimated") { - setText("performance-note", "Some costs are LiteLLM estimates; provider-reported OpenRouter costs are preferred when available."); + setText("performance-note", "Some historical costs are gateway estimates; provider-reported OpenRouter costs are preferred when available."); } else if (costKind === "unavailable" || (monthlyCost === undefined && cumulativeCost === undefined)) { - setText("performance-note", "Exact cost is unavailable for records without provider cost metadata. New LiteLLM responses are recorded going forward."); + setText("performance-note", "Exact cost is unavailable for records without provider cost metadata. New OpenRouter responses are recorded going forward."); } else if (costCoverage !== undefined && Number(costCoverage) < 1) { setText("performance-note", `Provider-reported cost coverage: ${formatPercent(costCoverage)}. Older or incomplete records remain unavailable.`); } diff --git a/src/blacki/memory/config.py b/src/blacki/memory/config.py index a454286..59843ed 100644 --- a/src/blacki/memory/config.py +++ b/src/blacki/memory/config.py @@ -7,12 +7,17 @@ from typing import TYPE_CHECKING, Any from urllib.parse import urlparse +from blacki.models.openrouter_ids import ( + canonical_openrouter_model_id, + is_openrouter_model_id, +) + if TYPE_CHECKING: from mem0 import Memory logger = logging.getLogger(__name__) -DEFAULT_LLM_MODEL = "openrouter/openai/gpt-oss-20b" +DEFAULT_LLM_MODEL = "openai/gpt-oss-20b" DEFAULT_EMBEDDER_PROVIDER = "gemini" DEFAULT_EMBEDDER_MODEL = "gemini-embedding-001" DEFAULT_EMBEDDER_DIMS = 768 @@ -87,12 +92,31 @@ def _get_llm_config() -> dict[str, Any]: else: return {} - llm_provider = _get_env_value("MEM0_LLM_PROVIDER") - if not llm_provider: - llm_provider = "litellm" if openrouter_key else "openai" + llm_provider = _get_env_value("MEM0_LLM_PROVIDER") or "openai" + if llm_provider.lower() == "litellm": + if not openrouter_key: + raise ValueError( + "MEM0_LLM_PROVIDER=litellm is no longer supported. " + "Set MEM0_LLM_PROVIDER=openai or remove the override." + ) + logger.warning( + "Mapping legacy MEM0_LLM_PROVIDER=litellm to openai for OpenRouter" + ) + llm_provider = "openai" + + if llm_provider.lower() == "openai": + llm_model = canonical_openrouter_model_id(llm_model) llm_config: dict[str, Any] = {"model": llm_model} + if llm_provider.lower() == "openai" and openrouter_key: + if not is_openrouter_model_id(llm_model): + raise ValueError( + "MEM0_LLM_MODEL must be an OpenRouter model ID (author/slug) " + "when OPENROUTER_API_KEY is set." + ) + llm_config["openrouter_base_url"] = "https://openrouter.ai/api/v1" + api_key = _get_env_value("MEM0_LLM_API_KEY") if not api_key: api_key = openrouter_key diff --git a/src/blacki/models/capabilities.py b/src/blacki/models/capabilities.py index 701f714..9742a7d 100644 --- a/src/blacki/models/capabilities.py +++ b/src/blacki/models/capabilities.py @@ -18,6 +18,8 @@ import httpx +from .openrouter_ids import OPENROUTER_PREFIX, canonical_openrouter_model_id + logger = logging.getLogger(__name__) OPENROUTER_MODELS_BASE_URL = "https://openrouter.ai/api/v1/model" @@ -71,24 +73,27 @@ def normalize_openrouter_model_id( ) -> str | None: """Return the OpenRouter API identifier for a model reference. - LiteLLM model strings use ``openrouter//`` while the - OpenRouter Models API uses ``/``. Exactly one transport - prefix is removed. Bare provider IDs are accepted only when the caller - explicitly states that the model is routed through OpenRouter; this avoids - making a native Google or future provider lookup by accident. + Blacki model references historically use ``openrouter//`` + while the OpenRouter Models API uses ``/``. Exactly one + transport prefix is removed. Dynamic router identifiers such as + ``openrouter/auto`` are already valid API IDs and are preserved. Bare + provider IDs are accepted only when the caller explicitly states that the + model is routed through OpenRouter; this avoids making a native Google or + future provider lookup by accident. """ if not isinstance(model_id, str): return None - normalized = model_id.strip() - if not normalized: + stripped = model_id.strip() + if not stripped: return None - prefix = "openrouter/" - if normalized.lower().startswith(prefix): - normalized = normalized[len(prefix) :] - elif not openrouter_routed: + if stripped.lower().startswith(OPENROUTER_PREFIX): + normalized = canonical_openrouter_model_id(stripped) + elif openrouter_routed: + normalized = stripped + else: return None # OpenRouter model IDs are author/slug. Keep variant suffixes (for diff --git a/src/blacki/models/factory.py b/src/blacki/models/factory.py index dfd4db5..9f83e4f 100644 --- a/src/blacki/models/factory.py +++ b/src/blacki/models/factory.py @@ -4,16 +4,25 @@ import logging import os -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING + +from blacki.utils.exceptions import ConfigurationError + +from .openrouter_ids import is_openrouter_model_id logger = logging.getLogger("blacki.agent") if TYPE_CHECKING: - from google.adk.models.lite_llm import LiteLlm + from .openrouter import OpenRouterLlm def normalize_model_for_openrouter(model_name: str) -> str: - """Map common model IDs to the OpenRouter/LiteLLM form.""" + """Map common model IDs to the OpenRouter transport form. + + The ``openrouter/`` prefix is retained for compatibility with stored + Telegram preferences and documented deployment samples. The adapter + strips it before calling OpenRouter's API. + """ normalized = model_name.strip() lower = normalized.lower() if lower.startswith("openrouter/"): @@ -25,34 +34,37 @@ def normalize_model_for_openrouter(model_name: str) -> str: return normalized -def build_model() -> str | LiteLlm: - """Build the configured native or LiteLLM model.""" - openrouter_api_key = os.getenv("OPENROUTER_API_KEY") - - model_name = os.getenv("ROOT_AGENT_MODEL", "gemini-2.5-flash") - model: str | LiteLlm = model_name +def build_model() -> str | OpenRouterLlm: + """Build the configured native Gemini or OpenRouter model.""" + openrouter_api_key = os.getenv("OPENROUTER_API_KEY", "").strip() + model_name = os.getenv("ROOT_AGENT_MODEL", "gemini-2.5-flash").strip() - use_litellm = openrouter_api_key is not None or "/" in model_name.lower() if openrouter_api_key: - model_name = normalize_model_for_openrouter(model_name) - - if use_litellm: - try: - from google.adk.models import LiteLlm + normalized = normalize_model_for_openrouter(model_name) + if not is_openrouter_model_id(normalized): + raise ConfigurationError( + f"ROOT_AGENT_MODEL={model_name!r} is not a valid OpenRouter " + "model ID. Use an author/slug identifier such as " + "google/gemini-2.5-flash." + ) + from .openrouter import OpenRouterLlm - litellm_kwargs: dict[str, Any] = {} - if model_name.lower().startswith("openrouter/") and openrouter_api_key: - litellm_kwargs["api_key"] = openrouter_api_key - from blacki.observability.costs import CostAwareLiteLLMClient + logger.info("Using OpenRouter model: %s", normalized) + return OpenRouterLlm(model=normalized, api_key=openrouter_api_key) - litellm_kwargs["llm_client"] = CostAwareLiteLLMClient() + if model_name.lower().startswith("openrouter/") or ( + "/" in model_name and not model_name.startswith("models/") + ): + raise ConfigurationError( + f"OPENROUTER_API_KEY is required for model {model_name!r}. " + "Set the key or use a native Gemini model such as " + "gemini-2.5-flash." + ) - logger.info("Using LiteLlm for model: %s", model_name) - return LiteLlm(model=model_name, **litellm_kwargs) - except ImportError: - logger.warning( - "LiteLlm not available, falling back to string model name. " - "OpenRouter models may not work." - ) + if not model_name.lower().startswith(("gemini-", "gemma-", "models/")): + raise ConfigurationError( + f"ROOT_AGENT_MODEL={model_name!r} is not a native Gemini model. " + "Set OPENROUTER_API_KEY for OpenRouter models." + ) - return model + return model_name diff --git a/src/blacki/models/inference.py b/src/blacki/models/inference.py index 3e6ff0c..b151d41 100644 --- a/src/blacki/models/inference.py +++ b/src/blacki/models/inference.py @@ -1,11 +1,11 @@ """Request-scoped model and reasoning controls for ADK turns. The ADK ``LlmRequest`` does not expose a provider-neutral field for arbitrary -LiteLLM parameters. OpenRouter's reasoning object is therefore carried in -``GenerateContentConfig.http_options.extra_body`` and translated by ADK's -LiteLLM adapter at the request boundary. This module keeps the profile -immutable and request-scoped so concurrent Telegram turns cannot change one -another's model settings. +OpenRouter parameters. OpenRouter's reasoning object is therefore carried in +``GenerateContentConfig.http_options.extra_body`` and merged into the request +body by Blacki's OpenRouter adapter. This module keeps the profile immutable +and request-scoped so concurrent Telegram turns cannot change one another's +model settings. """ from __future__ import annotations diff --git a/src/blacki/models/openrouter.py b/src/blacki/models/openrouter.py new file mode 100644 index 0000000..e38bb00 --- /dev/null +++ b/src/blacki/models/openrouter.py @@ -0,0 +1,162 @@ +"""Minimal OpenRouter model adapter for Google ADK. + +This replaces LiteLLM for Blacki's OpenRouter traffic. The adapter uses the +OpenAI-compatible chat-completions API directly, translates ADK requests at the +edge, and records provider-reported usage in the local cost ledger. + +Plaintext reasoning is echoed back on assistant messages. Encrypted +``reasoning_details`` signatures are not preserved, matching the previous +adapter's behavior. +""" + +from __future__ import annotations + +import logging +from collections.abc import AsyncGenerator, Callable +from pathlib import Path +from typing import Any + +from google.adk.models.base_llm import BaseLlm +from google.adk.models.llm_request import LlmRequest +from google.adk.models.llm_response import LlmResponse +from openai import AsyncOpenAI +from pydantic import PrivateAttr + +from blacki.observability.costs import observe_response +from blacki.observability.ledger import default_usage_ledger_path + +from .openrouter_ids import canonical_openrouter_model_id +from .openrouter_request import build_chat_request +from .openrouter_response import ( + OpenRouterStreamAccumulator, + response_to_llm_response, +) + +logger = logging.getLogger(__name__) + +DEFAULT_BASE_URL = "https://openrouter.ai/api/v1" +DEFAULT_TIMEOUT_SECONDS = 120.0 +DEFAULT_MAX_RETRIES = 2 + +ClientFactory = Callable[[float, int], AsyncOpenAI] + + +class OpenRouterLlm(BaseLlm): + """ADK model backed by OpenRouter's OpenAI-compatible endpoint. + + Credentials and transport settings are private attributes so they never + appear in ``repr()``, ``model_dump()``, or serialized ADK state. A client + is created per model call and closed in a ``finally`` block; storing an + ``AsyncOpenAI`` instance on a pydantic model would break deep copying and + leak unclosed transports across event loops. + """ + + _api_key: str = PrivateAttr() + _base_url: str = PrivateAttr() + _timeout_seconds: float = PrivateAttr() + _max_retries: int = PrivateAttr() + _ledger_path: Path = PrivateAttr() + _client_factory: ClientFactory | None = PrivateAttr(default=None) + + def __init__( + self, + model: str, + *, + api_key: str, + base_url: str = DEFAULT_BASE_URL, + timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS, + max_retries: int = DEFAULT_MAX_RETRIES, + ledger_path: Path | str | None = None, + client_factory: ClientFactory | None = None, + ) -> None: + super().__init__(model=model) + + key = api_key.strip() + if not key: + raise ValueError("OpenRouter API key is required.") + if timeout_seconds <= 0: + raise ValueError("OpenRouter timeout must be positive.") + if max_retries < 0: + raise ValueError("OpenRouter max_retries must be non-negative.") + + self._api_key = key + self._base_url = base_url.rstrip("/") + self._timeout_seconds = float(timeout_seconds) + self._max_retries = int(max_retries) + self._ledger_path = ( + Path(ledger_path) + if ledger_path is not None + else default_usage_ledger_path() + ) + self._client_factory = client_factory + + async def generate_content_async( + self, + llm_request: LlmRequest, + stream: bool = False, + ) -> AsyncGenerator[LlmResponse, None]: + """Generate one model turn through OpenRouter.""" + + self._maybe_append_user_content(llm_request) + + requested_model = llm_request.model or self.model + api_model = canonical_openrouter_model_id(requested_model) + request = build_chat_request(llm_request, model=api_model) + + timeout_seconds = ( + request.timeout_seconds + if request.timeout_seconds is not None + else self._timeout_seconds + ) + max_retries = ( + request.max_retries + if request.max_retries is not None + else self._max_retries + ) + client = self._create_client(timeout_seconds, max_retries) + try: + if stream: + async for response in self._stream( + client, + request.create_kwargs(stream=True), + ledger_model=requested_model, + ): + yield response + else: + completion = await client.chat.completions.create( + **request.create_kwargs(stream=False) + ) + raw = completion.model_dump(exclude_none=True) + await observe_response(raw, requested_model, self._ledger_path) + yield response_to_llm_response(raw) + finally: + await client.close() + + async def _stream( + self, + client: AsyncOpenAI, + create_kwargs: dict[str, Any], + *, + ledger_model: str, + ) -> AsyncGenerator[LlmResponse, None]: + stream = await client.chat.completions.create(**create_kwargs) + accumulator = OpenRouterStreamAccumulator() + try: + async for chunk in stream: + raw = chunk.model_dump(exclude_none=True) + await observe_response(raw, ledger_model, self._ledger_path) + for response in accumulator.add(raw): + yield response + finally: + await stream.close() + yield accumulator.finish() + + def _create_client(self, timeout_seconds: float, max_retries: int) -> AsyncOpenAI: + if self._client_factory is not None: + return self._client_factory(timeout_seconds, max_retries) + return AsyncOpenAI( + api_key=self._api_key, + base_url=self._base_url, + timeout=timeout_seconds, + max_retries=max_retries, + ) diff --git a/src/blacki/models/openrouter_ids.py b/src/blacki/models/openrouter_ids.py new file mode 100644 index 0000000..e52b2b7 --- /dev/null +++ b/src/blacki/models/openrouter_ids.py @@ -0,0 +1,35 @@ +"""Canonical OpenRouter model identifier handling.""" + +from __future__ import annotations + +OPENROUTER_PREFIX = "openrouter/" + + +def canonical_openrouter_model_id(model: str) -> str: + """Return the model identifier accepted by OpenRouter's API. + + Blacki historically stored model references with an ``openrouter/`` + transport prefix. Strip exactly one prefix when the remainder still looks + like an ``author/slug`` identifier. Dynamic router identifiers such as + ``openrouter/auto`` are already valid API IDs and must not be reduced to + ``auto``. + """ + + stripped = model.strip() + if stripped.lower().startswith(OPENROUTER_PREFIX): + remainder = stripped[len(OPENROUTER_PREFIX) :] + if "/" in remainder: + return remainder + return stripped + + +def is_openrouter_model_id(model: str) -> bool: + """Return whether ``model`` is a usable OpenRouter ``author/slug`` id.""" + + candidate = canonical_openrouter_model_id(model) + if candidate.count("/") != 1: + return False + author, slug = candidate.split("/", 1) + return bool( + author and slug and author not in {".", ".."} and slug not in {".", ".."} + ) diff --git a/src/blacki/models/openrouter_request.py b/src/blacki/models/openrouter_request.py new file mode 100644 index 0000000..a7abbff --- /dev/null +++ b/src/blacki/models/openrouter_request.py @@ -0,0 +1,564 @@ +"""Translate ADK model requests into OpenRouter chat-completion payloads. + +The conversion intentionally supports the features Blacki uses: system +instructions, text, inline images, function tools, function calls, function +responses, reasoning settings carried in ``http_options.extra_body``, and the +standard sampling parameters. Provider-specific or structured-output +configuration is rejected explicitly instead of being silently ignored. +""" + +from __future__ import annotations + +import base64 +import json +import logging +import uuid +from collections.abc import Mapping, Sequence +from dataclasses import dataclass, field +from typing import Any, cast + +from google.adk.models.llm_request import LlmRequest +from google.genai import types + +from .openrouter_ids import canonical_openrouter_model_id + +logger = logging.getLogger(__name__) + +_MISSING_TOOL_RESULT = '{"error":"Missing tool response."}' + +# The SDK merges ``extra_body`` over explicit keyword arguments. These keys +# define the conversation itself, so a stray extra_body value must never be +# able to replace them. +_RESERVED_EXTRA_BODY_KEYS = frozenset( + { + "model", + "messages", + "stream", + "stream_options", + "tools", + "tool_choice", + "response_format", + "max_tokens", + "temperature", + "top_p", + "stop", + "presence_penalty", + "frequency_penalty", + "seed", + } +) +_FILTERED_HEADER_NAMES = frozenset({"authorization", "content-type"}) + + +class OpenRouterRequestError(ValueError): + """Raised when an ADK request cannot be represented as an OpenRouter call.""" + + +@dataclass(slots=True) +class ChatRequest: + """One prepared OpenRouter chat-completions call.""" + + model: str + messages: list[dict[str, Any]] + generation: dict[str, Any] = field(default_factory=dict) + tools: list[dict[str, Any]] | None = None + extra_body: dict[str, Any] = field(default_factory=dict) + extra_headers: dict[str, str] = field(default_factory=dict) + timeout_seconds: float | None = None + max_retries: int | None = None + + def create_kwargs(self, *, stream: bool) -> dict[str, Any]: + """Return keyword arguments for ``chat.completions.create``.""" + + kwargs: dict[str, Any] = { + "model": self.model, + "messages": self.messages, + **self.generation, + "stream": stream, + } + if stream: + kwargs["stream_options"] = {"include_usage": True} + if self.tools: + kwargs["tools"] = self.tools + kwargs["tool_choice"] = "auto" + if self.extra_body: + kwargs["extra_body"] = dict(self.extra_body) + if self.extra_headers: + kwargs["extra_headers"] = dict(self.extra_headers) + return kwargs + + +def build_chat_request( + llm_request: LlmRequest, + *, + model: str, +) -> ChatRequest: + """Build one OpenRouter request from an ADK ``LlmRequest``.""" + + config = llm_request.config + _reject_unsupported_config(config) + + messages = contents_to_messages(llm_request.contents) + system_text = _system_instruction_text(config.system_instruction) + if system_text: + messages.insert(0, {"role": "system", "content": system_text}) + + extra_body, extra_headers, timeout_seconds, max_retries = _http_options( + config.http_options + ) + if config.top_k is not None: + extra_body["top_k"] = config.top_k + + return ChatRequest( + model=canonical_openrouter_model_id(model), + messages=messages, + generation=_generation_params(config), + tools=_tool_declarations(config.tools), + extra_body=extra_body, + extra_headers=extra_headers, + timeout_seconds=timeout_seconds, + max_retries=max_retries, + ) + + +def contents_to_messages( + contents: Sequence[types.Content], +) -> list[dict[str, Any]]: + """Convert ADK conversation contents into OpenAI chat messages. + + Function responses become ``role=tool`` messages. When a content block + mixes function responses with text or images (ADK's multimodal tool-result + plugin does this), the tool messages are emitted first and the remaining + parts become a separate user message. + """ + + messages: list[dict[str, Any]] = [] + pending_calls: list[tuple[str | None, str]] = [] + fallback_counter = 0 + + for content in contents: + parts = content.parts or [] + if not parts: + continue + + if content.role == "model": + message, calls = _assistant_message(parts) + if message is not None: + messages.append(message) + pending_calls.extend(calls) + continue + + for part in parts: + response = part.function_response + if response is None: + continue + call_id = _resolve_tool_response_id( + response, + pending_calls, + fallback_counter, + ) + fallback_counter += 1 + messages.append( + { + "role": "tool", + "tool_call_id": call_id, + "content": _tool_response_text(response), + } + ) + + remaining = [part for part in parts if part.function_response is None] + if remaining: + user_message = _user_message(remaining) + if user_message is not None: + messages.append(user_message) + + return _ensure_tool_results(messages) + + +def _reject_unsupported_config(config: types.GenerateContentConfig) -> None: + unsupported = { + "response_schema": config.response_schema, + "response_mime_type": config.response_mime_type, + "tool_config": config.tool_config, + "safety_settings": config.safety_settings, + "thinking_config": config.thinking_config, + } + for name, value in unsupported.items(): + if value is not None: + raise OpenRouterRequestError( + f"Blacki's OpenRouter adapter does not support {name}." + ) + candidate_count = config.candidate_count + if candidate_count is not None and candidate_count != 1: + raise OpenRouterRequestError( + "Blacki's OpenRouter adapter only supports candidate_count=1." + ) + + +def _generation_params(config: types.GenerateContentConfig) -> dict[str, Any]: + params: dict[str, Any] = {} + if config.temperature is not None: + params["temperature"] = config.temperature + if config.max_output_tokens is not None: + params["max_tokens"] = config.max_output_tokens + if config.top_p is not None: + params["top_p"] = config.top_p + if config.stop_sequences: + params["stop"] = list(config.stop_sequences) + if config.presence_penalty is not None: + params["presence_penalty"] = config.presence_penalty + if config.frequency_penalty is not None: + params["frequency_penalty"] = config.frequency_penalty + if config.seed is not None: + params["seed"] = config.seed + return params + + +def _http_options( + http_options: types.HttpOptions | None, +) -> tuple[dict[str, Any], dict[str, str], float | None, int | None]: + if http_options is None: + return {}, {}, None, None + + extra_body: dict[str, Any] = {} + if http_options.extra_body is not None: + if not isinstance(http_options.extra_body, Mapping): + raise OpenRouterRequestError("http_options.extra_body must be a mapping.") + for key, value in http_options.extra_body.items(): + key_text = str(key) + if key_text.lower() in _RESERVED_EXTRA_BODY_KEYS: + logger.warning( + "Ignoring reserved extra_body key %r for OpenRouter", + key_text, + ) + continue + extra_body[key_text] = value + + headers: dict[str, str] = {} + if http_options.headers is not None: + if not isinstance(http_options.headers, Mapping): + raise OpenRouterRequestError("http_options.headers must be a mapping.") + for key, value in http_options.headers.items(): + key_text = str(key) + if key_text.lower() in _FILTERED_HEADER_NAMES: + logger.warning( + "Ignoring caller-supplied %r header for OpenRouter", + key_text, + ) + continue + headers[key_text] = str(value) + + timeout_seconds: float | None = None + if http_options.timeout is not None: + if http_options.timeout <= 0: + raise OpenRouterRequestError("http_options.timeout must be positive.") + timeout_seconds = http_options.timeout / 1000.0 + + max_retries: int | None = None + retry_options = http_options.retry_options + if retry_options is not None and retry_options.attempts is not None: + if retry_options.attempts < 1: + raise OpenRouterRequestError( + "http_options.retry_options.attempts must be at least 1." + ) + max_retries = retry_options.attempts - 1 + + return extra_body, headers, timeout_seconds, max_retries + + +def _system_instruction_text(value: object) -> str | None: + if value is None: + return None + if isinstance(value, str): + text = value.strip() + return text or None + if isinstance(value, types.Content): + text = _join_text_parts(value.parts or []) + return text or None + if isinstance(value, (list, tuple)): + text = _join_text_parts(value) + return text or None + raise OpenRouterRequestError( + f"Unsupported system_instruction type: {type(value).__name__}" + ) + + +def _join_text_parts(parts: Sequence[types.Part]) -> str: + texts: list[str] = [] + for part in parts: + if part.inline_data is not None or part.file_data is not None: + raise OpenRouterRequestError( + "Media parts in a system instruction are not supported." + ) + if part.text and not part.thought: + texts.append(part.text) + return "".join(texts).strip() + + +def _assistant_message( + parts: Sequence[types.Part], +) -> tuple[dict[str, Any] | None, list[tuple[str | None, str]]]: + texts: list[str] = [] + thoughts: list[str] = [] + tool_calls: list[dict[str, Any]] = [] + calls: list[tuple[str | None, str]] = [] + + for part in parts: + function_call = part.function_call + if function_call is not None: + name = function_call.name + if not name: + raise OpenRouterRequestError( + "An assistant function call is missing its name." + ) + call_id = function_call.id or _new_call_id() + tool_calls.append( + { + "id": call_id, + "type": "function", + "function": { + "name": name, + "arguments": _json_dumps(function_call.args or {}), + }, + } + ) + calls.append((name, call_id)) + continue + + if part.inline_data is not None or part.file_data is not None: + raise OpenRouterRequestError( + "Assistant media content is not supported by OpenRouter." + ) + + if part.text: + if part.thought: + thoughts.append(part.text) + else: + texts.append(part.text) + + if not texts and not thoughts and not tool_calls: + return None, calls + + message: dict[str, Any] = { + "role": "assistant", + "content": "".join(texts) or None, + } + if thoughts: + message["reasoning"] = "".join(thoughts) + if tool_calls: + message["tool_calls"] = tool_calls + return message, calls + + +def _user_message(parts: Sequence[types.Part]) -> dict[str, Any] | None: + items: list[dict[str, Any]] = [] + texts: list[str] = [] + + for part in parts: + if part.text and not part.thought: + items.append({"type": "text", "text": part.text}) + texts.append(part.text) + continue + + inline_data = part.inline_data + if inline_data is not None: + mime_type = (inline_data.mime_type or "").strip() + if mime_type.lower() == "image/jpg": + mime_type = "image/jpeg" + if not mime_type.lower().startswith("image/"): + raise OpenRouterRequestError( + f"Unsupported inline media type: {mime_type or 'unknown'}" + ) + data = inline_data.data + if not isinstance(data, (bytes, bytearray)) or not data: + raise OpenRouterRequestError("Inline image data is missing.") + encoded = base64.b64encode(bytes(data)).decode("ascii") + items.append( + { + "type": "image_url", + "image_url": {"url": f"data:{mime_type};base64,{encoded}"}, + } + ) + continue + + if part.file_data is not None: + raise OpenRouterRequestError( + "File URI content is not supported by OpenRouter." + ) + + if not items: + return None + if len(texts) == len(items): + return {"role": "user", "content": "".join(texts)} + return {"role": "user", "content": items} + + +def _tool_response_text(response: types.FunctionResponse) -> str: + payload: object = response.response + if isinstance(payload, str): + return payload + try: + return json.dumps( + payload if payload is not None else {}, + ensure_ascii=False, + default=str, + ) + except (TypeError, ValueError) as exc: + raise OpenRouterRequestError( + "A tool response could not be serialized as JSON." + ) from exc + + +def _resolve_tool_response_id( + response: types.FunctionResponse, + pending_calls: list[tuple[str | None, str]], + fallback_counter: int, +) -> str: + response_id = response.id + if response_id: + for index, (_, call_id) in enumerate(pending_calls): + if call_id == response_id: + pending_calls.pop(index) + return response_id + + name = response.name + for index, (call_name, call_id) in enumerate(pending_calls): + if call_name == name: + pending_calls.pop(index) + return call_id + + if pending_calls: + return pending_calls.pop(0)[1] + return response_id or f"blacki_tool_{fallback_counter}" + + +def _ensure_tool_results( + messages: list[dict[str, Any]], +) -> list[dict[str, Any]]: + healed: list[dict[str, Any]] = [] + pending_ids: list[str] = [] + + for message in messages: + role = message.get("role") + if pending_ids and role != "tool": + _insert_missing_tool_results(healed, pending_ids) + pending_ids = [] + + if role == "assistant": + pending_ids = [ + str(call["id"]) + for call in message.get("tool_calls", []) + if call.get("id") + ] + elif role == "tool": + call_id = message.get("tool_call_id") + if call_id in pending_ids: + pending_ids.remove(call_id) + + healed.append(message) + + if pending_ids: + _insert_missing_tool_results(healed, pending_ids) + return healed + + +def _insert_missing_tool_results( + messages: list[dict[str, Any]], + pending_ids: list[str], +) -> None: + logger.warning("Missing tool results for tool_call_id(s): %s", pending_ids) + for call_id in pending_ids: + messages.append( + { + "role": "tool", + "tool_call_id": call_id, + "content": _MISSING_TOOL_RESULT, + } + ) + + +def _tool_declarations( + tools: Sequence[object] | None, +) -> list[dict[str, Any]] | None: + if not tools: + return None + + declarations: list[dict[str, Any]] = [] + for tool in tools: + if not isinstance(tool, types.Tool): + raise OpenRouterRequestError( + f"Unsupported tool type: {type(tool).__name__}" + ) + data = tool.model_dump(exclude_none=True) + if not data: + continue + extra_fields = set(data) - {"function_declarations"} + if extra_fields: + fields = ", ".join(sorted(extra_fields)) + raise OpenRouterRequestError( + f"OpenRouter supports function tools only; received: {fields}" + ) + function_declarations = tool.function_declarations or [] + for declaration in function_declarations: + declarations.append(_tool_for(declaration)) + return declarations or None + + +def _tool_for(declaration: types.FunctionDeclaration) -> dict[str, Any]: + name = declaration.name + if not name: + raise OpenRouterRequestError("A function declaration is missing its name.") + return { + "type": "function", + "function": { + "name": name, + "description": declaration.description or "", + "parameters": _declaration_schema(declaration), + }, + } + + +def _declaration_schema(declaration: types.FunctionDeclaration) -> dict[str, Any]: + if declaration.parameters_json_schema: + return cast(dict[str, Any], declaration.parameters_json_schema) + if declaration.parameters is not None: + normalized = _lowercase_schema_types( + declaration.parameters.model_dump(by_alias=True, exclude_none=True) + ) + return normalized if isinstance(normalized, dict) else {} + return {"type": "object", "properties": {}} + + +def _lowercase_schema_types(value: object) -> object: + if isinstance(value, dict): + normalized: dict[str, Any] = {} + for key, item in value.items(): + if key == "type": + normalized[key] = _normalize_schema_type(item) + else: + normalized[key] = _lowercase_schema_types(item) + return normalized + if isinstance(value, list): + return [_lowercase_schema_types(item) for item in value] + return value + + +def _normalize_schema_type(value: object) -> object: + if isinstance(value, types.Type): + return value.value.lower() + if isinstance(value, str): + return value.lower() + return value + + +def _json_dumps(value: object) -> str: + try: + return json.dumps(value, ensure_ascii=False, default=str) + except (TypeError, ValueError) as exc: + raise OpenRouterRequestError( + "A function-call argument could not be serialized as JSON." + ) from exc + + +def _new_call_id() -> str: + return f"blacki_call_{uuid.uuid4().hex}" diff --git a/src/blacki/models/openrouter_response.py b/src/blacki/models/openrouter_response.py new file mode 100644 index 0000000..c120cfc --- /dev/null +++ b/src/blacki/models/openrouter_response.py @@ -0,0 +1,500 @@ +"""Convert OpenRouter chat-completion payloads into ADK responses.""" + +from __future__ import annotations + +import json +import logging +import uuid +from collections.abc import Mapping +from dataclasses import dataclass, field +from typing import Any + +from google.adk.models.llm_response import LlmResponse +from google.genai import types + +logger = logging.getLogger(__name__) + +_FINISH_REASONS = { + "stop": types.FinishReason.STOP, + "length": types.FinishReason.MAX_TOKENS, + "tool_calls": types.FinishReason.STOP, + "function_call": types.FinishReason.STOP, + "content_filter": types.FinishReason.SAFETY, +} +_TRUNCATED_TOOL_MESSAGE = ( + "Tool call arguments were truncated and could not be parsed as JSON. " + "Increase max_output_tokens and retry." +) + + +class OpenRouterResponseError(RuntimeError): + """Raised when OpenRouter returns a payload Blacki cannot interpret.""" + + +class _TruncatedToolArgumentsError(OpenRouterResponseError): + """Raised when a tool call was cut off by the token limit.""" + + +def response_to_llm_response(payload: Mapping[str, Any]) -> LlmResponse: + """Convert one non-streaming OpenRouter response into an ``LlmResponse``.""" + + _raise_for_error(payload) + model_version = _model_version(payload) + choices = payload.get("choices") + if not isinstance(choices, list) or not choices: + return LlmResponse( + content=types.Content(role="model", parts=[]), + partial=False, + model_version=model_version, + ) + + choice = choices[0] + if not isinstance(choice, Mapping): + raise OpenRouterResponseError("OpenRouter returned an invalid choice.") + finish_reason = _finish_reason_value(choice.get("finish_reason")) + if finish_reason == "error": + raise OpenRouterResponseError( + "OpenRouter returned finish_reason=error without an error payload." + ) + + message = choice.get("message") + if not isinstance(message, Mapping): + message = {} + usage = payload.get("usage") + try: + return _response_from_message( + message, + finish_reason=finish_reason, + usage=usage, + model_version=model_version, + ) + except _TruncatedToolArgumentsError: + return _truncated_response(model_version, usage) + + +def _response_from_message( + message: Mapping[str, Any], + *, + finish_reason: str | None, + usage: object, + model_version: str | None, +) -> LlmResponse: + parts: list[types.Part] = [] + reasoning = _reasoning_text(message) + if reasoning: + parts.append(types.Part(text=reasoning, thought=True)) + + text = _content_text(message.get("content")) + if text: + parts.append(types.Part.from_text(text=text)) + + tool_calls = message.get("tool_calls") + if isinstance(tool_calls, list): + for call in tool_calls: + if not isinstance(call, Mapping): + continue + function = call.get("function") + if not isinstance(function, Mapping): + raise OpenRouterResponseError( + "OpenRouter returned a tool call without a function." + ) + name = function.get("name") + if not isinstance(name, str) or not name: + raise OpenRouterResponseError( + "OpenRouter returned a tool call without a name." + ) + args = _parse_arguments(function.get("arguments"), finish_reason) + call_id = call.get("id") + if not isinstance(call_id, str) or not call_id: + call_id = _new_call_id() + parts.append( + types.Part( + function_call=types.FunctionCall( + id=call_id, + name=name, + args=args, + ) + ) + ) + + response = LlmResponse( + content=types.Content(role="model", parts=parts), + partial=False, + model_version=model_version, + ) + _apply_finish_reason(response, finish_reason) + response.usage_metadata = _usage_metadata(usage) + return response + + +def _apply_finish_reason( + response: LlmResponse, + finish_reason: str | None, +) -> None: + mapped = _map_finish_reason(finish_reason) + if mapped is None: + return + response.finish_reason = mapped + if mapped != types.FinishReason.STOP: + response.error_code = mapped + response.error_message = _finish_error_message(mapped) + + +def _map_finish_reason(value: object) -> types.FinishReason | None: + if value is None or value == "": + return None + if isinstance(value, types.FinishReason): + return value + return _FINISH_REASONS.get(str(value).lower(), types.FinishReason.OTHER) + + +def _finish_error_message(finish_reason: types.FinishReason) -> str: + if finish_reason == types.FinishReason.MAX_TOKENS: + return "Maximum tokens reached" + return f"Finished with {finish_reason.name}" + + +def _finish_reason_value(value: object) -> str | None: + if isinstance(value, str) and value: + return value + return None + + +def _model_version(payload: Mapping[str, Any]) -> str | None: + model = payload.get("model") + return model if isinstance(model, str) and model else None + + +def _raise_for_error(payload: Mapping[str, Any]) -> None: + error = payload.get("error") + if error is not None: + raise OpenRouterResponseError(_error_message(error)) + + +def _error_message(error: object) -> str: + if isinstance(error, Mapping): + code = error.get("code") + message = error.get("message") + parts: list[str] = [] + if code is not None: + parts.append(str(code)) + if message is not None: + parts.append(str(message)) + text = ": ".join(parts) or "Unknown OpenRouter error" + else: + text = str(error) + return text[:1000] + + +def _truncated_response( + model_version: str | None, + usage: object, +) -> LlmResponse: + return LlmResponse( + content=types.Content(role="model", parts=[]), + error_code=types.FinishReason.MAX_TOKENS, + error_message=_TRUNCATED_TOOL_MESSAGE, + finish_reason=types.FinishReason.MAX_TOKENS, + partial=False, + model_version=model_version, + usage_metadata=_usage_metadata(usage), + ) + + +def _reasoning_text(value: Mapping[str, Any]) -> str: + for key in ("reasoning", "reasoning_content"): + raw = value.get(key) + if isinstance(raw, str) and raw: + return raw + + details = value.get("reasoning_details") + if not isinstance(details, list): + return "" + + texts: list[str] = [] + for detail in details: + if not isinstance(detail, Mapping): + continue + detail_type = detail.get("type") + if detail_type == "reasoning.text": + text = detail.get("text") + elif detail_type == "reasoning.summary": + text = detail.get("summary") + else: + continue + if isinstance(text, str) and text: + texts.append(text) + return "".join(texts) + + +def _content_text(value: object) -> str: + if isinstance(value, str): + return value + if not isinstance(value, list): + return "" + texts: list[str] = [] + for item in value: + if not isinstance(item, Mapping): + continue + if item.get("type") != "text": + continue + text = item.get("text") + if isinstance(text, str): + texts.append(text) + return "".join(texts) + + +def _parse_arguments( + value: object, + finish_reason: str | None, +) -> dict[str, Any]: + if value is None: + return {} + if isinstance(value, Mapping): + return dict(value) + if not isinstance(value, str): + raise OpenRouterResponseError( + "OpenRouter returned tool arguments that are not JSON text." + ) + stripped = value.strip() + if not stripped: + return {} + try: + parsed = json.loads(stripped) + except json.JSONDecodeError as exc: + if finish_reason == "length": + raise _TruncatedToolArgumentsError(_TRUNCATED_TOOL_MESSAGE) from exc + raise OpenRouterResponseError( + "OpenRouter returned invalid JSON in tool arguments." + ) from exc + if not isinstance(parsed, dict): + raise OpenRouterResponseError( + "OpenRouter returned tool arguments that are not a JSON object." + ) + return parsed + + +def _usage_metadata( + usage: object, +) -> types.GenerateContentResponseUsageMetadata | None: + if not isinstance(usage, Mapping) or not usage: + return None + + prompt_tokens = _nonnegative_int(usage.get("prompt_tokens")) + completion_tokens = _nonnegative_int(usage.get("completion_tokens")) + total_tokens = _nonnegative_int(usage.get("total_tokens")) + + prompt_details = usage.get("prompt_tokens_details") + cached_tokens = ( + _nonnegative_int(prompt_details.get("cached_tokens")) + if isinstance(prompt_details, Mapping) + else None + ) + + completion_details = usage.get("completion_tokens_details") + reasoning_tokens = ( + _nonnegative_int(completion_details.get("reasoning_tokens")) + if isinstance(completion_details, Mapping) + else None + ) + + if ( + prompt_tokens is None + and completion_tokens is None + and total_tokens is None + and cached_tokens is None + and reasoning_tokens is None + ): + return None + + return types.GenerateContentResponseUsageMetadata( + prompt_token_count=prompt_tokens or 0, + candidates_token_count=completion_tokens or 0, + total_token_count=total_tokens or 0, + cached_content_token_count=cached_tokens, + thoughts_token_count=reasoning_tokens or None, + ) + + +def _nonnegative_int(value: object) -> int | None: + if value is None or isinstance(value, bool): + return None + if isinstance(value, int): + number = value + elif isinstance(value, float): + number = int(value) + elif isinstance(value, str): + try: + number = int(value) + except ValueError: + return None + else: + return None + return number if number >= 0 else None + + +def _new_call_id() -> str: + return f"blacki_call_{uuid.uuid4().hex}" + + +@dataclass(slots=True) +class OpenRouterStreamAccumulator: + """Accumulate streamed deltas into partial and final ADK responses.""" + + _model_version: str | None = None + _finish_reason: str | None = None + _usage: Mapping[str, Any] | None = None + _text: list[str] = field(default_factory=list) + _reasoning: list[str] = field(default_factory=list) + _tool_calls: dict[int, dict[str, Any]] = field(default_factory=dict) + + def add(self, payload: Mapping[str, Any]) -> list[LlmResponse]: + """Consume one SSE payload and return any partial responses.""" + + _raise_for_error(payload) + model_version = _model_version(payload) + if model_version: + self._model_version = model_version + usage = payload.get("usage") + if isinstance(usage, Mapping) and usage: + self._usage = usage + + choices = payload.get("choices") + if not isinstance(choices, list): + return [] + + responses: list[LlmResponse] = [] + for choice in choices: + if not isinstance(choice, Mapping): + continue + finish_reason = _finish_reason_value(choice.get("finish_reason")) + if finish_reason: + self._finish_reason = finish_reason + delta = choice.get("delta") + if not isinstance(delta, Mapping): + continue + + reasoning = _reasoning_text(delta) + if reasoning: + self._reasoning.append(reasoning) + responses.append( + LlmResponse( + content=types.Content( + role="model", + parts=[types.Part(text=reasoning, thought=True)], + ), + partial=True, + model_version=self._model_version, + ) + ) + + text = _content_text(delta.get("content")) + if text: + self._text.append(text) + responses.append( + LlmResponse( + content=types.Content( + role="model", + parts=[types.Part.from_text(text=text)], + ), + partial=True, + model_version=self._model_version, + ) + ) + + tool_calls = delta.get("tool_calls") + if isinstance(tool_calls, list): + self._merge_tool_calls(tool_calls) + + return responses + + def finish(self) -> LlmResponse: + """Return the single aggregated response for the completed stream.""" + + if self._finish_reason == "error": + raise OpenRouterResponseError( + "OpenRouter stream ended with finish_reason=error." + ) + + parts: list[types.Part] = [] + reasoning = "".join(self._reasoning) + if reasoning: + parts.append(types.Part(text=reasoning, thought=True)) + text = "".join(self._text) + if text: + parts.append(types.Part.from_text(text=text)) + + try: + for index in sorted(self._tool_calls): + part = self._tool_call_part(self._tool_calls[index]) + if part is not None: + parts.append(part) + except _TruncatedToolArgumentsError: + return _truncated_response(self._model_version, self._usage) + + response = LlmResponse( + content=types.Content(role="model", parts=parts), + partial=False, + model_version=self._model_version, + usage_metadata=_usage_metadata(self._usage), + ) + finish_reason = self._finish_reason + if finish_reason is None and parts: + finish_reason = "stop" + _apply_finish_reason(response, finish_reason) + return response + + def _tool_call_part( + self, + entry: Mapping[str, Any], + ) -> types.Part | None: + name = entry.get("name") + arguments = entry.get("arguments") + if not isinstance(name, str) or not name: + if isinstance(arguments, str) and arguments: + raise OpenRouterResponseError( + "OpenRouter streamed tool arguments without a function name." + ) + return None + args = _parse_arguments(arguments, self._finish_reason) + call_id = entry.get("id") + if not isinstance(call_id, str) or not call_id: + call_id = _new_call_id() + return types.Part( + function_call=types.FunctionCall( + id=call_id, + name=name, + args=args, + ) + ) + + def _merge_tool_calls(self, tool_calls: list[object]) -> None: + for position, call in enumerate(tool_calls): + if not isinstance(call, Mapping): + continue + index = call.get("index") + if not isinstance(index, int): + index = position + entry = self._tool_calls.setdefault( + index, + {"id": None, "name": "", "arguments": ""}, + ) + + call_id = call.get("id") + if isinstance(call_id, str) and call_id and not entry["id"]: + entry["id"] = call_id + + function = call.get("function") + if not isinstance(function, Mapping): + continue + + name = function.get("name") + if isinstance(name, str) and name: + if not entry["name"]: + entry["name"] = name + elif name != entry["name"]: + entry["name"] += name + + arguments = function.get("arguments") + if isinstance(arguments, str) and arguments: + entry["arguments"] += arguments diff --git a/src/blacki/observability/costs.py b/src/blacki/observability/costs.py index dec0d0b..35b4fcd 100644 --- a/src/blacki/observability/costs.py +++ b/src/blacki/observability/costs.py @@ -1,4 +1,4 @@ -"""Capture provider-reported LiteLLM usage before ADK reduces the response.""" +"""Capture provider-reported OpenRouter usage before ADK reduces the response.""" from __future__ import annotations @@ -8,17 +8,16 @@ import sqlite3 import time import uuid -from collections.abc import AsyncIterator, Mapping +from collections.abc import Mapping from contextvars import ContextVar from dataclasses import dataclass from pathlib import Path from typing import Any -from google.adk.models.lite_llm import LiteLLMClient from google.adk.models.llm_response import LlmResponse from opentelemetry import trace -from .ledger import UsageRecord, default_usage_ledger_path, write_usage_record +from .ledger import UsageRecord, write_usage_record logger = logging.getLogger("blacki.llm_costs") @@ -74,7 +73,7 @@ def _nonnegative_integer(value: Any) -> int | None: def extract_cost_observation(response: Any, model: str) -> CostObservation | None: - """Extract raw provider cost and token fields from a LiteLLM response.""" + """Extract raw provider cost and token fields from an OpenRouter response.""" usage = _read_value(response, "usage") reported_cost = _nonnegative_number(_first_value(usage, ("cost", "total_cost"))) @@ -85,12 +84,6 @@ def extract_cost_observation(response: Any, model: str) -> CostObservation | Non ("upstream_inference_cost", "upstream_cost"), ) ) - hidden_params = _read_value(response, "_hidden_params") - estimated_cost = _nonnegative_number( - _first_value(hidden_params, ("response_cost", "cost")) - ) - if reported_cost is not None: - estimated_cost = None input_tokens = _nonnegative_integer( _first_value(usage, ("prompt_tokens", "input_tokens")) ) @@ -101,7 +94,6 @@ def extract_cost_observation(response: Any, model: str) -> CostObservation | Non if ( reported_cost is None and upstream_cost is None - and estimated_cost is None and input_tokens is None and output_tokens is None and total_tokens is None @@ -114,8 +106,6 @@ def extract_cost_observation(response: Any, model: str) -> CostObservation | Non cost_kind = ( "reported" if reported_cost is not None - else "estimated" - if estimated_cost is not None else "upstream_only" if upstream_cost is not None else "unknown" @@ -123,7 +113,7 @@ def extract_cost_observation(response: Any, model: str) -> CostObservation | Non cost_source = ( "provider_usage" if reported_cost is not None or upstream_cost is not None - else "litellm_response_cost" + else "usage_only" ) return { "model": str(model)[:256], @@ -133,7 +123,7 @@ def extract_cost_observation(response: Any, model: str) -> CostObservation | Non "total_tokens": total_tokens, "cost_usd": reported_cost, "upstream_cost_usd": upstream_cost, - "estimated_cost_usd": estimated_cost, + "estimated_cost_usd": None, "cost_kind": cost_kind, "cost_source": cost_source, "currency": "USD", @@ -199,11 +189,12 @@ def _remember_observation(observation: CostObservation) -> _CostCapture | None: return capture -async def _observe_response( +async def observe_response( response: Any, model: str, ledger_path: Path, ) -> None: + """Record one OpenRouter response payload in the local usage ledger.""" observation = extract_cost_observation(response, model) if observation is None: return @@ -253,41 +244,3 @@ def _attach_cost_metadata(response: LlmResponse) -> CostObservation | None: if not bool(getattr(response, "partial", False)): _ACTIVE_CAPTURE.set(None) return dict(observation) if observation is not None else None - - -class CostAwareLiteLLMClient(LiteLLMClient): - """Observe LiteLLM responses while preserving ADK's client contract.""" - - def __init__(self, ledger_path: Path | str | None = None) -> None: - super().__init__() - self.ledger_path = ( - Path(ledger_path) if ledger_path else default_usage_ledger_path() - ) - - async def _observed_stream( - self, - response: Any, - model: str, - ) -> AsyncIterator[Any]: - async for chunk in response: - await _observe_response(chunk, model, self.ledger_path) - yield chunk - - async def acompletion( - self, - model: Any, - messages: Any, - tools: Any, - **kwargs: Any, - ) -> Any: - response = await super().acompletion( - model=model, - messages=messages, - tools=tools, - **kwargs, - ) - model_name = str(model) - if kwargs.get("stream"): - return self._observed_stream(response, model_name) - await _observe_response(response, model_name, self.ledger_path) - return response diff --git a/src/blacki/server.py b/src/blacki/server.py index ac86273..9472f11 100644 --- a/src/blacki/server.py +++ b/src/blacki/server.py @@ -604,7 +604,7 @@ def main() -> None: RELOAD_AGENTS: Whether to reload agents on file changes (true/false) AGENT_ENGINE: Agent Engine instance for session and memory SQLITE_PATH: Path to SQLite database (default: {AGENT_DIR}/.adk/tools.db) - OPENROUTER_API_KEY: Key for LiteLLM/OpenRouter + OPENROUTER_API_KEY: Key for OpenRouter model access ALLOW_ORIGINS: JSON array string of allowed CORS origins HOST: Server host (default: 127.0.0.1, set to 0.0.0.0 for containers) PORT: Server port (default: 8080) diff --git a/src/blacki/tools/brave_search.py b/src/blacki/tools/brave_search.py index e10e231..7e11c7d 100644 --- a/src/blacki/tools/brave_search.py +++ b/src/blacki/tools/brave_search.py @@ -58,8 +58,8 @@ async def brave_search( """Search the web using Brave Search API. This is a model-agnostic alternative to Google Search that works with - any LLM provider via LiteLLM/OpenRouter. Set BRAVE_SEARCH_API_KEY - in your environment. + Blacki's OpenRouter-routed or native Gemini model. Set + BRAVE_SEARCH_API_KEY in your environment. Args: query: The search query string. diff --git a/src/blacki/utils/config.py b/src/blacki/utils/config.py index ac9c0ea..5420f80 100644 --- a/src/blacki/utils/config.py +++ b/src/blacki/utils/config.py @@ -134,7 +134,7 @@ class ServerEnv(BaseModel): openrouter_api_key: str | None = Field( default=None, alias="OPENROUTER_API_KEY", - description="OpenRouter API key for LiteLLM integration", + description="OpenRouter API key for model access", ) allow_origins: str = Field( diff --git a/tests/memory/test_config.py b/tests/memory/test_config.py index 4a4b246..d807fef 100644 --- a/tests/memory/test_config.py +++ b/tests/memory/test_config.py @@ -36,19 +36,69 @@ def test_returns_empty_when_no_model_or_openrouter( assert result == {} - def test_uses_litellm_openrouter_default_when_no_model( + def test_uses_openrouter_default_when_no_model( self, monkeypatch: pytest.MonkeyPatch ) -> None: - """Should use LiteLLM and OpenRouter default when no MEM0_LLM_MODEL.""" + """Should use the OpenAI provider and OpenRouter default.""" monkeypatch.delenv("MEM0_LLM_MODEL", raising=False) monkeypatch.delenv("MEM0_LLM_PROVIDER", raising=False) monkeypatch.setenv("OPENROUTER_API_KEY", "test_key") result = _get_llm_config() - assert result["provider"] == "litellm" - assert result["config"]["model"] == "openrouter/openai/gpt-oss-20b" + assert result["provider"] == "openai" + assert result["config"]["model"] == "openai/gpt-oss-20b" assert result["config"]["api_key"] == "test_key" + assert result["config"]["openrouter_base_url"] == "https://openrouter.ai/api/v1" + + def test_strips_openrouter_prefix_from_mem0_model( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Should convert historical LiteLLM-style model IDs.""" + monkeypatch.setenv("MEM0_LLM_MODEL", "openrouter/google/gemini-2.0-flash-001") + monkeypatch.delenv("MEM0_LLM_PROVIDER", raising=False) + monkeypatch.setenv("OPENROUTER_API_KEY", "test_key") + + result = _get_llm_config() + + assert result["config"]["model"] == "google/gemini-2.0-flash-001" + + def test_maps_legacy_litellm_provider( + self, + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, + ) -> None: + """Should map the removed LiteLLM provider to OpenAI for OpenRouter.""" + monkeypatch.setenv("MEM0_LLM_MODEL", "openai/gpt-oss-20b") + monkeypatch.setenv("MEM0_LLM_PROVIDER", "litellm") + monkeypatch.setenv("OPENROUTER_API_KEY", "test_key") + + result = _get_llm_config() + + assert result["provider"] == "openai" + assert "Mapping legacy MEM0_LLM_PROVIDER" in caplog.text + + def test_rejects_legacy_litellm_provider_without_openrouter( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Should fail clearly instead of silently switching providers.""" + monkeypatch.setenv("MEM0_LLM_MODEL", "gpt-4o-mini") + monkeypatch.setenv("MEM0_LLM_PROVIDER", "litellm") + monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) + + with pytest.raises(ValueError, match="no longer supported"): + _get_llm_config() + + def test_rejects_non_openrouter_model_with_openrouter_key( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Should reject bare model names that OpenRouter cannot route.""" + monkeypatch.setenv("MEM0_LLM_MODEL", "gpt-oss-20b") + monkeypatch.delenv("MEM0_LLM_PROVIDER", raising=False) + monkeypatch.setenv("OPENROUTER_API_KEY", "test_key") + + with pytest.raises(ValueError, match="author/slug"): + _get_llm_config() def test_uses_custom_model_and_provider( self, monkeypatch: pytest.MonkeyPatch @@ -65,7 +115,7 @@ def test_uses_custom_model_and_provider( def test_uses_mem0_llm_api_key(self, monkeypatch: pytest.MonkeyPatch) -> None: """Should prefer MEM0_LLM_API_KEY over OPENROUTER_API_KEY.""" - monkeypatch.setenv("MEM0_LLM_MODEL", "test-model") + monkeypatch.setenv("MEM0_LLM_MODEL", "openai/test-model") monkeypatch.setenv("MEM0_LLM_API_KEY", "mem0_key") monkeypatch.setenv("OPENROUTER_API_KEY", "openrouter_key") @@ -77,6 +127,7 @@ def test_includes_temperature(self, monkeypatch: pytest.MonkeyPatch) -> None: """Should include temperature when set.""" monkeypatch.setenv("MEM0_LLM_MODEL", "test-model") monkeypatch.setenv("MEM0_LLM_TEMPERATURE", "0.5") + monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) result = _get_llm_config() @@ -88,6 +139,7 @@ def test_invalid_temperature_logs_warning( """Should log warning for invalid temperature.""" monkeypatch.setenv("MEM0_LLM_MODEL", "test-model") monkeypatch.setenv("MEM0_LLM_TEMPERATURE", "invalid") + monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) result = _get_llm_config() @@ -98,6 +150,7 @@ def test_includes_max_tokens(self, monkeypatch: pytest.MonkeyPatch) -> None: """Should include max_tokens when set.""" monkeypatch.setenv("MEM0_LLM_MODEL", "test-model") monkeypatch.setenv("MEM0_LLM_MAX_TOKENS", "2000") + monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) result = _get_llm_config() @@ -109,6 +162,7 @@ def test_invalid_max_tokens_logs_warning( """Should log warning for invalid max_tokens.""" monkeypatch.setenv("MEM0_LLM_MODEL", "test-model") monkeypatch.setenv("MEM0_LLM_MAX_TOKENS", "invalid") + monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) result = _get_llm_config() @@ -255,7 +309,7 @@ class TestBuildOssConfig: def test_builds_full_config(self, monkeypatch: pytest.MonkeyPatch) -> None: """Should build full OSS config from environment.""" - monkeypatch.setenv("MEM0_LLM_MODEL", "test-model") + monkeypatch.setenv("MEM0_LLM_MODEL", "openai/test-model") monkeypatch.setenv("OPENROUTER_API_KEY", "test_key") monkeypatch.setenv("MEM0_COLLECTION_NAME", "test_collection") monkeypatch.setenv("MEM0_QDRANT_URL", "https://cluster.qdrant.io") @@ -263,7 +317,7 @@ def test_builds_full_config(self, monkeypatch: pytest.MonkeyPatch) -> None: result = _build_oss_config() - assert result["llm"]["provider"] == "litellm" + assert result["llm"]["provider"] == "openai" assert result["embedder"]["provider"] == "gemini" assert result["vector_store"]["provider"] == "qdrant" assert result["vector_store"]["config"]["collection_name"] == "test_collection" diff --git a/tests/test_deployment_contract.py b/tests/test_deployment_contract.py index 602d9d0..d8cef8d 100644 --- a/tests/test_deployment_contract.py +++ b/tests/test_deployment_contract.py @@ -4,6 +4,7 @@ import shutil import subprocess import sys +import tomllib from pathlib import Path from typing import Any @@ -71,6 +72,22 @@ def test_docker_context_is_an_explicit_allowlist() -> None: assert "!.env" not in rules +def test_litellm_is_absent_from_declared_and_locked_dependencies() -> None: + """Keep the removed LiteLLM dependency out of runtime and dev installs.""" + pyproject = tomllib.loads(_read("pyproject.toml")) + declared = [ + dependency.lower() + for dependency in ( + pyproject["project"]["dependencies"] + pyproject["dependency-groups"]["dev"] + ) + ] + assert not any("litellm" in dependency for dependency in declared) + + lock = tomllib.loads(_read("uv.lock")) + package_names = {package["name"] for package in lock["package"]} + assert "litellm" not in package_names + + def test_minimal_environment_is_safe_and_complete() -> None: """The golden-path sample selects one provider and private host defaults.""" values = _parse_env(".env.minimal") diff --git a/tests/test_integration.py b/tests/test_integration.py index 19411f4..7f70145 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -19,7 +19,7 @@ AUTO_COMPACTION_TOKEN_THRESHOLD, ) from blacki.models.factory import build_model -from blacki.observability.costs import CostAwareLiteLLMClient +from blacki.models.openrouter import OpenRouterLlm class AgentConfigLike(Protocol): @@ -103,7 +103,7 @@ def test_agent_has_required_configuration(self) -> None: # Required: agent model assert typed_agent.model is not None - # model can be a string name or a model object (e.g. LiteLlm) + # model can be a string name or a model object (e.g. OpenRouterLlm) if isinstance(typed_agent.model, str): assert len(typed_agent.model) > 0 else: @@ -157,11 +157,11 @@ def test_memory_tools_are_registered(self) -> None: assert "save_memory" in tool_names assert "search_memory" in tool_names - def test_litellm_models_use_cost_aware_client(self, monkeypatch) -> None: + def test_openrouter_models_use_openrouter_adapter(self, monkeypatch) -> None: monkeypatch.setenv("OPENROUTER_API_KEY", "test-key") monkeypatch.setenv("ROOT_AGENT_MODEL", "google/gemini-2.5-flash") model = build_model() - assert not isinstance(model, str) - assert isinstance(model.llm_client, CostAwareLiteLLMClient) + assert isinstance(model, OpenRouterLlm) + assert model.model == "openrouter/google/gemini-2.5-flash" diff --git a/tests/test_llm_costs.py b/tests/test_llm_costs.py index 6202db4..e268529 100644 --- a/tests/test_llm_costs.py +++ b/tests/test_llm_costs.py @@ -1,22 +1,18 @@ -"""Tests for provider cost capture at the LiteLLM boundary.""" +"""Tests for provider cost capture at the OpenRouter boundary.""" from __future__ import annotations -from collections.abc import AsyncIterator from pathlib import Path -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import MagicMock, patch import pytest -from google.adk.models.lite_llm import LiteLLMClient from google.adk.models.llm_response import LlmResponse -from google.genai import types from blacki.observability.costs import ( - CostAwareLiteLLMClient, - _observe_response, _remember_observation, _set_span_cost_attributes, extract_cost_observation, + observe_response, ) from blacki.observability.ledger import read_usage_ledger from blacki.observability.lifecycle import attach_cost_metadata, begin_cost_capture @@ -45,19 +41,25 @@ def test_extract_cost_observation_preserves_provider_and_upstream_costs() -> Non assert observation["upstream_cost_usd"] == 0.0022 assert observation["estimated_cost_usd"] is None assert observation["cost_kind"] == "reported" + assert observation["cost_source"] == "provider_usage" -def test_extract_cost_observation_distinguishes_estimated_unknown_and_invalid() -> None: - estimated = extract_cost_observation( +def test_extract_cost_observation_distinguishes_upstream_unknown_and_invalid() -> None: + upstream = extract_cost_observation( { - "usage": {"prompt_tokens": 3, "completion_tokens": 2}, - "_hidden_params": {"response_cost": 0.0004}, + "usage": { + "prompt_tokens": 3, + "completion_tokens": 2, + "cost_details": {"upstream_inference_cost": 0.0004}, + }, }, "openai/test", ) - assert estimated is not None - assert estimated["estimated_cost_usd"] == 0.0004 - assert estimated["cost_kind"] == "estimated" + assert upstream is not None + assert upstream["cost_usd"] is None + assert upstream["upstream_cost_usd"] == 0.0004 + assert upstream["cost_kind"] == "upstream_only" + assert upstream["cost_source"] == "provider_usage" unknown = extract_cost_observation( {"id": "unknown", "usage": {"total_tokens": 5}}, @@ -66,6 +68,7 @@ def test_extract_cost_observation_distinguishes_estimated_unknown_and_invalid() assert unknown is not None assert unknown["cost_usd"] is None assert unknown["cost_kind"] == "unknown" + assert unknown["cost_source"] == "usage_only" invalid = extract_cost_observation( {"usage": {"cost": "not-a-number", "total_tokens": 5}}, @@ -124,8 +127,8 @@ def test_remember_observation_preserves_reported_cost_and_handles_no_capture() - async def test_observe_response_handles_missing_capture_and_write_errors( tmp_path: Path, caplog: pytest.LogCaptureFixture ) -> None: - await _observe_response({}, "openrouter/test", tmp_path / "costs.db") - await _observe_response( + await observe_response({}, "openrouter/test", tmp_path / "costs.db") + await observe_response( {"usage": {"total_tokens": 1}}, "openrouter/test", tmp_path / "costs.db" ) @@ -135,7 +138,7 @@ async def test_observe_response_handles_missing_capture_and_write_errors( "blacki.observability.costs.write_usage_record", side_effect=OSError("read-only"), ): - await _observe_response( + await observe_response( {"usage": {"total_tokens": 1, "cost": 0.001}}, "openrouter/test", tmp_path / "costs.db", @@ -164,46 +167,31 @@ def test_partial_response_keeps_capture_until_final_response() -> None: @pytest.mark.asyncio -async def test_non_stream_response_is_ledgered_and_attached_to_adk_event( - tmp_path: Path, -) -> None: - raw_response = { - "id": "generation-1", - "usage": { - "prompt_tokens": 10, - "completion_tokens": 5, - "total_tokens": 15, - "cost": 0.01, - "cost_details": {"upstream_inference_cost": 0.009}, - }, - } +async def test_observe_response_persists_reported_cost(tmp_path: Path) -> None: begin_cost_capture( user_id="user-1", session_id="session-1", invocation_id="inv-1", ) - client = CostAwareLiteLLMClient(tmp_path / "costs.db") - with patch.object( - LiteLLMClient, - "acompletion", - new=AsyncMock(return_value=raw_response), - ): - returned = await client.acompletion( - model="openrouter/test", - messages=[], - tools=[], - stream=False, - ) - - assert returned is raw_response - response = LlmResponse( - content=types.Content(role="model", parts=[types.Part.from_text(text="ok")]) + await observe_response( + { + "id": "generation-1", + "usage": { + "prompt_tokens": 10, + "completion_tokens": 5, + "total_tokens": 15, + "cost": 0.01, + "cost_details": {"upstream_inference_cost": 0.009}, + }, + }, + "openrouter/test", + tmp_path / "costs.db", ) - metadata = attach_cost_metadata(response) + + metadata = attach_cost_metadata(LlmResponse()) assert metadata is not None assert metadata["cost_usd"] == 0.01 - assert response.custom_metadata is not None - assert response.custom_metadata["blacki.cost"]["dedupe_key"].startswith("provider:") + assert metadata["dedupe_key"].startswith("provider:") ledger = read_usage_ledger( tmp_path / "costs.db", @@ -218,12 +206,21 @@ async def test_non_stream_response_is_ledgered_and_attached_to_adk_event( @pytest.mark.asyncio -async def test_streaming_final_usage_updates_one_idempotent_ledger_row( +async def test_repeated_stream_usage_updates_one_idempotent_ledger_row( tmp_path: Path, ) -> None: - async def response_stream() -> AsyncIterator[dict[str, object]]: - yield {"id": "generation-stream", "usage": {"total_tokens": 4}} - yield { + begin_cost_capture( + user_id="user-stream", + session_id="session-stream", + invocation_id="inv-stream", + ) + await observe_response( + {"id": "generation-stream", "usage": {"total_tokens": 4}}, + "openrouter/test", + tmp_path / "costs.db", + ) + await observe_response( + { "id": "generation-stream", "usage": { "prompt_tokens": 3, @@ -231,28 +228,11 @@ async def response_stream() -> AsyncIterator[dict[str, object]]: "total_tokens": 4, "cost": 0.004, }, - } - - begin_cost_capture( - user_id="user-stream", - session_id="session-stream", - invocation_id="inv-stream", + }, + "openrouter/test", + tmp_path / "costs.db", ) - client = CostAwareLiteLLMClient(tmp_path / "costs.db") - with patch.object( - LiteLLMClient, - "acompletion", - new=AsyncMock(return_value=response_stream()), - ): - returned = await client.acompletion( - model="openrouter/test", - messages=[], - tools=[], - stream=True, - ) - chunks = [chunk async for chunk in returned] - assert len(chunks) == 2 response = LlmResponse() assert attach_cost_metadata(response)["cost_usd"] == 0.004 # type: ignore[index] ledger = read_usage_ledger( diff --git a/tests/test_model_capabilities.py b/tests/test_model_capabilities.py index c510d1a..c12be29 100644 --- a/tests/test_model_capabilities.py +++ b/tests/test_model_capabilities.py @@ -65,6 +65,8 @@ def _payload(**overrides: Any) -> dict[str, Any]: ("openai/gpt-5.6-luna", True, "openai/gpt-5.6-luna"), ("gpt-5.6-luna", True, None), ("openrouter/openrouter/openai/gpt-5.6-luna", False, None), + ("openrouter/auto", False, "openrouter/auto"), + ("openrouter/free", False, "openrouter/free"), ("google/gemini-2.5-flash", False, None), ("", True, None), (123, True, None), diff --git a/tests/test_model_factory.py b/tests/test_model_factory.py index 84e4cca..4b4714d 100644 --- a/tests/test_model_factory.py +++ b/tests/test_model_factory.py @@ -2,11 +2,11 @@ from __future__ import annotations -from unittest.mock import patch - import pytest from blacki.models.factory import build_model, normalize_model_for_openrouter +from blacki.models.openrouter import OpenRouterLlm +from blacki.utils.exceptions import ConfigurationError @pytest.mark.parametrize( @@ -40,24 +40,74 @@ def test_build_model_keeps_native_model_without_openrouter( assert build_model() == "gemini-2.5-flash" -def test_build_model_uses_litellm_without_openrouter_api_key( +def test_build_model_uses_openrouter_adapter_with_key( monkeypatch: pytest.MonkeyPatch, ) -> None: - monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) + monkeypatch.setenv("OPENROUTER_API_KEY", "test-key") monkeypatch.setenv("ROOT_AGENT_MODEL", "google/gemini-2.5-flash") model = build_model() - assert not isinstance(model, str) - assert model.model == "google/gemini-2.5-flash" - assert model.llm_client is not None + assert isinstance(model, OpenRouterLlm) + assert model.model == "openrouter/google/gemini-2.5-flash" + + +def test_build_model_keeps_existing_openrouter_prefix( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("OPENROUTER_API_KEY", "test-key") + monkeypatch.setenv("ROOT_AGENT_MODEL", "openrouter/openai/gpt-4o") + + model = build_model() + + assert isinstance(model, OpenRouterLlm) + assert model.model == "openrouter/openai/gpt-4o" + + +def test_build_model_rejects_openrouter_model_without_key( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) + monkeypatch.setenv("ROOT_AGENT_MODEL", "openrouter/google/gemini-2.5-flash") + + with pytest.raises(ConfigurationError, match="OPENROUTER_API_KEY is required"): + build_model() -def test_build_model_falls_back_when_litellm_is_unavailable( +def test_build_model_rejects_provider_style_model_without_key( monkeypatch: pytest.MonkeyPatch, ) -> None: monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) monkeypatch.setenv("ROOT_AGENT_MODEL", "google/gemini-2.5-flash") - with patch("builtins.__import__", side_effect=ImportError("LiteLlm missing")): - assert build_model() == "google/gemini-2.5-flash" + with pytest.raises(ConfigurationError, match="OPENROUTER_API_KEY is required"): + build_model() + + +def test_build_model_rejects_invalid_openrouter_id_with_key( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setenv("OPENROUTER_API_KEY", "test-key") + monkeypatch.setenv("ROOT_AGENT_MODEL", "custom-model") + + with pytest.raises(ConfigurationError, match="not a valid OpenRouter"): + build_model() + + +def test_build_model_rejects_unknown_native_model_without_key( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) + monkeypatch.setenv("ROOT_AGENT_MODEL", "custom-model") + + with pytest.raises(ConfigurationError, match="not a native Gemini model"): + build_model() + + +def test_build_model_accepts_models_prefixed_native_model( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("OPENROUTER_API_KEY", raising=False) + monkeypatch.setenv("ROOT_AGENT_MODEL", "models/gemini-2.5-flash") + + assert build_model() == "models/gemini-2.5-flash" diff --git a/tests/test_openrouter_ids.py b/tests/test_openrouter_ids.py new file mode 100644 index 0000000..d026e21 --- /dev/null +++ b/tests/test_openrouter_ids.py @@ -0,0 +1,47 @@ +"""Tests for canonical OpenRouter model identifiers.""" + +from __future__ import annotations + +import pytest + +from blacki.models.openrouter_ids import ( + canonical_openrouter_model_id, + is_openrouter_model_id, +) + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + ("openrouter/openai/gpt-4o", "openai/gpt-4o"), + ("openrouter/google/gemini-2.5-flash", "google/gemini-2.5-flash"), + ("OPENROUTER/OpenAI/GPT-4O", "OpenAI/GPT-4O"), + ("openrouter/auto", "openrouter/auto"), + ("openrouter/free", "openrouter/free"), + ("google/gemini-2.5-flash", "google/gemini-2.5-flash"), + (" openrouter/openai/gpt-4o ", "openai/gpt-4o"), + ("custom-model", "custom-model"), + ("", ""), + ], +) +def test_canonical_openrouter_model_id(value: str, expected: str) -> None: + assert canonical_openrouter_model_id(value) == expected + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + ("openrouter/openai/gpt-4o", True), + ("openai/gpt-4o", True), + ("openrouter/auto", True), + ("openrouter/free", True), + ("custom-model", False), + ("", False), + ("openrouter/", False), + ("openrouter/../gpt", False), + ("openrouter/gpt/..", False), + ("author/slug/extra", False), + ], +) +def test_is_openrouter_model_id(value: str, expected: bool) -> None: + assert is_openrouter_model_id(value) is expected diff --git a/tests/test_openrouter_llm.py b/tests/test_openrouter_llm.py new file mode 100644 index 0000000..6718d29 --- /dev/null +++ b/tests/test_openrouter_llm.py @@ -0,0 +1,452 @@ +"""Integration tests for Blacki's OpenRouter ADK adapter.""" + +from __future__ import annotations + +import copy +import json +from collections.abc import Callable +from pathlib import Path +from typing import Any + +import httpx +import openai +import pytest +from google.adk.models.llm_request import LlmRequest +from google.adk.models.llm_response import LlmResponse +from google.genai import types +from openai import AsyncOpenAI + +from blacki.models.openrouter import OpenRouterLlm +from blacki.models.openrouter_request import OpenRouterRequestError +from blacki.observability.ledger import read_usage_ledger +from blacki.observability.lifecycle import attach_cost_metadata, begin_cost_capture + +RequestRecord = dict[str, Any] + + +def _llm( + handler: Callable[[httpx.Request], httpx.Response], + *, + tmp_path: Path, + capture: list[dict[str, Any]] | None = None, + clients: list[AsyncOpenAI] | None = None, + model: str = "openrouter/google/gemini-2.5-flash", +) -> OpenRouterLlm: + transport = httpx.MockTransport(handler) + + def factory(timeout: float, max_retries: int) -> AsyncOpenAI: + if capture is not None: + capture.append({"timeout": timeout, "max_retries": max_retries}) + client = AsyncOpenAI( + api_key="test-key", + base_url="https://openrouter.ai/api/v1", + http_client=httpx.AsyncClient(transport=transport), + timeout=timeout, + max_retries=max_retries, + ) + if clients is not None: + clients.append(client) + return client + + return OpenRouterLlm( + model=model, + api_key="test-key", + ledger_path=tmp_path / "costs.db", + client_factory=factory, + ) + + +def _handler( + records: list[RequestRecord], + response_body: dict[str, Any] | None = None, + *, + status_code: int = 200, + sse: str | None = None, +) -> Callable[[httpx.Request], httpx.Response]: + def handle(request: httpx.Request) -> httpx.Response: + records.append( + { + "url": str(request.url), + "headers": dict(request.headers), + "body": json.loads(request.content), + } + ) + if sse is not None: + return httpx.Response( + 200, + headers={"content-type": "text/event-stream"}, + text=sse, + ) + return httpx.Response(status_code, json=response_body or {}) + + return handle + + +def _request(*, model: str | None = None, config: Any = None) -> LlmRequest: + return LlmRequest( + model=model, + contents=[ + types.Content( + role="user", + parts=[types.Part.from_text(text="hello")], + ) + ], + config=config or types.GenerateContentConfig(), + ) + + +@pytest.mark.asyncio +async def test_non_streaming_request_and_response_mapping(tmp_path: Path) -> None: + records: list[RequestRecord] = [] + response_body = { + "id": "gen-1", + "model": "google/gemini-2.5-flash", + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "hi"}, + "finish_reason": "stop", + } + ], + "usage": {"prompt_tokens": 3, "completion_tokens": 1, "total_tokens": 4}, + } + llm = _llm(_handler(records, response_body), tmp_path=tmp_path) + request = _request( + config=types.GenerateContentConfig( + http_options=types.HttpOptions(extra_body={"reasoning": {"effort": "low"}}) + ) + ) + + responses = [response async for response in llm.generate_content_async(request)] + + assert len(responses) == 1 + assert responses[0].content is not None + assert responses[0].content.parts is not None + assert responses[0].content.parts[0].text == "hi" + assert responses[0].usage_metadata is not None + assert responses[0].usage_metadata.total_token_count == 4 + + assert len(records) == 1 + assert records[0]["url"] == "https://openrouter.ai/api/v1/chat/completions" + assert records[0]["headers"]["authorization"] == "Bearer test-key" + assert records[0]["body"] == { + "model": "google/gemini-2.5-flash", + "messages": [{"role": "user", "content": "hello"}], + "stream": False, + "reasoning": {"effort": "low"}, + } + + +@pytest.mark.asyncio +async def test_per_turn_model_override_and_prefix_are_stripped(tmp_path: Path) -> None: + records: list[RequestRecord] = [] + llm = _llm( + _handler( + records, + { + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "ok"}, + "finish_reason": "stop", + } + ] + }, + ), + tmp_path=tmp_path, + ) + + request = _request(model="openrouter/openai/gpt-4o") + responses = [response async for response in llm.generate_content_async(request)] + + assert records[0]["body"]["model"] == "openai/gpt-4o" + assert responses[0].content is not None + + +@pytest.mark.asyncio +async def test_streaming_yields_partials_and_final_aggregate(tmp_path: Path) -> None: + records: list[RequestRecord] = [] + sse = ( + ": OPENROUTER PROCESSING\n\n" + 'data: {"id":"gen-1","model":"google/gemini-2.5-flash",' + '"choices":[{"index":0,"delta":{"reasoning":"think"},' + '"finish_reason":null}]}\n\n' + 'data: {"id":"gen-1","choices":[{"index":0,' + '"delta":{"content":"Hel"},"finish_reason":null}]}\n\n' + 'data: {"id":"gen-1","choices":[{"index":0,' + '"delta":{"content":"lo"},"finish_reason":"stop"}]}\n\n' + 'data: {"id":"gen-1","choices":[{"index":0,' + '"delta":{"content":""},"finish_reason":"stop"}],' + '"usage":{"prompt_tokens":2,"completion_tokens":2,"total_tokens":4}}\n\n' + "data: [DONE]\n\n" + ) + clients: list[AsyncOpenAI] = [] + llm = _llm( + _handler(records, sse=sse), + tmp_path=tmp_path, + clients=clients, + ) + + responses = [ + response + async for response in llm.generate_content_async(_request(), stream=True) + ] + + assert len(responses) == 4 + assert responses[0].partial is True + assert responses[0].content is not None + assert responses[0].content.parts is not None + assert responses[0].content.parts[0].thought is True + assert responses[1].partial is True + assert responses[1].content is not None + assert responses[1].content.parts is not None + assert responses[1].content.parts[0].text == "Hel" + assert responses[2].partial is True + assert responses[2].content is not None + assert responses[2].content.parts is not None + assert responses[2].content.parts[0].text == "lo" + assert responses[3].partial is False + assert responses[3].content is not None + assert responses[3].content.parts is not None + assert responses[3].content.parts[0].text == "think" + assert responses[3].content.parts[1].text == "Hello" + assert responses[3].usage_metadata is not None + assert responses[3].usage_metadata.total_token_count == 4 + + assert records[0]["body"]["stream"] is True + assert records[0]["body"]["stream_options"] == {"include_usage": True} + assert clients[0].is_closed() is True + + +@pytest.mark.asyncio +async def test_http_error_propagates_and_client_is_closed(tmp_path: Path) -> None: + records: list[RequestRecord] = [] + clients: list[AsyncOpenAI] = [] + llm = _llm( + _handler(records, {"error": {"message": "bad key"}}, status_code=401), + tmp_path=tmp_path, + clients=clients, + ) + + with pytest.raises(openai.AuthenticationError): + [response async for response in llm.generate_content_async(_request())] + + assert clients[0].is_closed() is True + + +@pytest.mark.asyncio +async def test_unsupported_config_fails_before_network(tmp_path: Path) -> None: + records: list[RequestRecord] = [] + llm = _llm(_handler(records, {}), tmp_path=tmp_path) + + with pytest.raises(OpenRouterRequestError, match="response_mime_type"): + [ + response + async for response in llm.generate_content_async( + _request( + config=types.GenerateContentConfig( + response_mime_type="application/json" + ) + ) + ) + ] + + assert records == [] + + +@pytest.mark.asyncio +async def test_cost_observation_writes_prefixed_model_to_ledger( + tmp_path: Path, +) -> None: + llm = _llm( + _handler( + [], + { + "id": "gen-cost", + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "hi"}, + "finish_reason": "stop", + } + ], + "usage": { + "prompt_tokens": 10, + "completion_tokens": 5, + "total_tokens": 15, + "cost": 0.01, + "cost_details": {"upstream_inference_cost": 0.009}, + }, + }, + ), + tmp_path=tmp_path, + ) + begin_cost_capture( + user_id="user-1", + session_id="session-1", + invocation_id="inv-1", + ) + + [response async for response in llm.generate_content_async(_request())] + metadata = attach_cost_metadata(LlmResponse()) + + assert metadata is not None + assert metadata["cost_usd"] == 0.01 + assert metadata["cost_source"] == "provider_usage" + + ledger = read_usage_ledger( + tmp_path / "costs.db", + selected_since=None, + selected_until=10**12, + month_start=0, + now=10**12, + ) + assert ledger.cumulative.records == 1 + assert ledger.users["user-1"].cost_nano_usd is not None + + +@pytest.mark.asyncio +async def test_streaming_cost_observation_writes_one_ledger_row( + tmp_path: Path, +) -> None: + sse = ( + 'data: {"id":"gen-stream","choices":[{"index":0,' + '"delta":{"content":"hi"},"finish_reason":"stop"}]}\n\n' + 'data: {"id":"gen-stream","choices":[{"index":0,' + '"delta":{"content":""},"finish_reason":"stop"}],' + '"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2,' + '"cost":0.002}}\n\n' + "data: [DONE]\n\n" + ) + llm = _llm(_handler([], sse=sse), tmp_path=tmp_path) + begin_cost_capture( + user_id="user-stream", + session_id="session-stream", + invocation_id="inv-stream", + ) + + [response async for response in llm.generate_content_async(_request(), stream=True)] + metadata = attach_cost_metadata(LlmResponse()) + + assert metadata is not None + assert metadata["cost_usd"] == 0.002 + + ledger = read_usage_ledger( + tmp_path / "costs.db", + selected_since=None, + selected_until=10**12, + month_start=0, + now=10**12, + ) + assert ledger.cumulative.records == 1 + + +def test_model_copy_repr_and_dump_do_not_expose_credentials(tmp_path: Path) -> None: + llm = _llm(lambda request: httpx.Response(200, json={}), tmp_path=tmp_path) + + copied = copy.deepcopy(llm) + model_copied = llm.model_copy(deep=True) + + for candidate in (llm, copied, model_copied): + assert "test-key" not in repr(candidate) + assert "test-key" not in str(candidate.model_dump()) + assert copied.model == llm.model + assert model_copied.model == llm.model + + +def test_constructor_validation(tmp_path: Path) -> None: + with pytest.raises(ValueError, match="API key"): + OpenRouterLlm(model="openai/gpt-4o", api_key=" ") + with pytest.raises(ValueError, match="timeout"): + OpenRouterLlm( + model="openai/gpt-4o", + api_key="key", + timeout_seconds=0, + ledger_path=tmp_path / "costs.db", + ) + with pytest.raises(ValueError, match="max_retries"): + OpenRouterLlm( + model="openai/gpt-4o", + api_key="key", + max_retries=-1, + ledger_path=tmp_path / "costs.db", + ) + + +@pytest.mark.asyncio +async def test_default_client_construction(tmp_path: Path) -> None: + llm = OpenRouterLlm( + model="openai/gpt-4o", + api_key="key", + ledger_path=str(tmp_path / "costs.db"), + base_url="https://openrouter.ai/api/v1/", + ) + + client = llm._create_client(5.0, 0) + try: + assert isinstance(client, AsyncOpenAI) + assert str(client.base_url).rstrip("/") == "https://openrouter.ai/api/v1" + finally: + await client.close() + + +@pytest.mark.asyncio +async def test_per_request_timeout_and_retry_options_are_forwarded( + tmp_path: Path, +) -> None: + captures: list[dict[str, Any]] = [] + llm = _llm( + _handler( + [], + { + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "ok"}, + "finish_reason": "stop", + } + ] + }, + ), + tmp_path=tmp_path, + capture=captures, + ) + + request = _request( + config=types.GenerateContentConfig( + http_options=types.HttpOptions( + timeout=5_000, + retry_options=types.HttpRetryOptions(attempts=4), + ) + ) + ) + [response async for response in llm.generate_content_async(request)] + + assert captures == [{"timeout": 5.0, "max_retries": 3}] + + +@pytest.mark.asyncio +async def test_default_timeout_and_retries_are_used_without_http_options( + tmp_path: Path, +) -> None: + captures: list[dict[str, Any]] = [] + llm = _llm( + _handler( + [], + { + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": "ok"}, + "finish_reason": "stop", + } + ] + }, + ), + tmp_path=tmp_path, + capture=captures, + ) + + [response async for response in llm.generate_content_async(_request())] + + assert captures == [{"timeout": 120.0, "max_retries": 2}] diff --git a/tests/test_openrouter_request.py b/tests/test_openrouter_request.py new file mode 100644 index 0000000..cc6c3db --- /dev/null +++ b/tests/test_openrouter_request.py @@ -0,0 +1,652 @@ +"""Tests for translating ADK requests into OpenRouter payloads.""" + +from __future__ import annotations + +from typing import Any, cast + +import pytest +from google.adk.models.llm_request import LlmRequest +from google.genai import types + +from blacki.models.openrouter_request import ( + ChatRequest, + OpenRouterRequestError, + _system_instruction_text, + _tool_declarations, + build_chat_request, + contents_to_messages, +) + + +def _user(*parts: types.Part) -> types.Content: + return types.Content(role="user", parts=list(parts)) + + +def _model(*parts: types.Part) -> types.Content: + return types.Content(role="model", parts=list(parts)) + + +def _call( + name: str = "lookup", + *, + call_id: str | None = "call-1", + args: dict[str, Any] | None = None, +) -> types.Part: + return types.Part( + function_call=types.FunctionCall( + id=call_id, + name=name, + args=args or {"query": "weather"}, + ) + ) + + +def _response( + name: str = "lookup", + *, + call_id: str | None = "call-1", + response: dict[str, Any] | None = None, +) -> types.Part: + return types.Part( + function_response=types.FunctionResponse( + id=call_id, + name=name, + response=response or {"result": "sunny"}, + ) + ) + + +def test_text_only_user_content_uses_string() -> None: + messages = contents_to_messages( + [_user(types.Part.from_text(text="hello"), types.Part.from_text(text=" world"))] + ) + + assert messages == [{"role": "user", "content": "hello world"}] + + +def test_image_content_uses_data_url_and_normalizes_jpg() -> None: + messages = contents_to_messages( + [ + _user( + types.Part.from_text(text="what is this?"), + types.Part.from_bytes(data=b"image-bytes", mime_type="image/jpg"), + ) + ] + ) + + content = messages[0]["content"] + assert isinstance(content, list) + assert content[0] == {"type": "text", "text": "what is this?"} + assert content[1]["image_url"]["url"].startswith("data:image/jpeg;base64,") + + +def test_assistant_tool_call_and_tool_response_keep_matching_ids() -> None: + messages = contents_to_messages( + [ + _model(_call(call_id="call-1")), + _user(_response(call_id="call-1")), + ] + ) + + assert messages[0]["role"] == "assistant" + assert messages[0]["tool_calls"][0]["id"] == "call-1" + assert messages[0]["tool_calls"][0]["function"]["name"] == "lookup" + assert messages[1] == { + "role": "tool", + "tool_call_id": "call-1", + "content": '{"result": "sunny"}', + } + + +def test_missing_ids_are_synthesized_and_paired_by_name() -> None: + messages = contents_to_messages( + [ + _model(_call(call_id=None)), + _user(_response(call_id=None)), + ] + ) + + call_id = messages[0]["tool_calls"][0]["id"] + assert call_id.startswith("blacki_call_") + assert messages[1]["tool_call_id"] == call_id + + +def test_mismatched_response_id_falls_back_to_name_pairing() -> None: + messages = contents_to_messages( + [ + _model(_call(call_id="blacki-call-1")), + _user(_response(call_id="adk-response-1")), + ] + ) + + assert messages[0]["tool_calls"][0]["id"] == "blacki-call-1" + assert messages[1]["tool_call_id"] == "blacki-call-1" + + +def test_missing_tool_response_gets_placeholder_before_next_message() -> None: + messages = contents_to_messages( + [ + _model(_call(call_id="call-1")), + _user(types.Part.from_text(text="continue")), + ] + ) + + assert [message["role"] for message in messages] == ["assistant", "tool", "user"] + assert messages[1]["tool_call_id"] == "call-1" + + +def test_mixed_function_response_and_image_splits_into_tool_and_user_messages() -> None: + messages = contents_to_messages( + [ + _model(_call(call_id="call-1")), + _user( + _response(call_id="call-1"), + types.Part.from_bytes(data=b"png", mime_type="image/png"), + ), + ] + ) + + assert [message["role"] for message in messages] == ["assistant", "tool", "user"] + user_content = messages[2]["content"] + assert isinstance(user_content, list) + assert user_content[0]["type"] == "image_url" + + +def test_reasoning_parts_are_preserved_on_assistant_messages() -> None: + messages = contents_to_messages( + [ + _model( + types.Part(text="private reasoning", thought=True), + types.Part.from_text(text="public answer"), + ) + ] + ) + + assert messages[0] == { + "role": "assistant", + "content": "public answer", + "reasoning": "private reasoning", + } + + +def test_empty_contents_are_skipped() -> None: + assert contents_to_messages([types.Content(role="user", parts=[])]) == [] + + +def test_assistant_media_is_rejected() -> None: + with pytest.raises(OpenRouterRequestError, match="Assistant media"): + contents_to_messages( + [ + _model( + types.Part.from_bytes(data=b"png", mime_type="image/png"), + ) + ] + ) + + +def test_function_call_without_name_is_rejected() -> None: + with pytest.raises(OpenRouterRequestError, match="missing its name"): + contents_to_messages( + [ + _model( + types.Part( + function_call=types.FunctionCall(name=None, args={}), + ) + ) + ] + ) + + +def test_non_image_inline_data_is_rejected() -> None: + with pytest.raises(OpenRouterRequestError, match="Unsupported inline media"): + contents_to_messages( + [ + _user( + types.Part.from_bytes( + data=b"%PDF", + mime_type="application/pdf", + ) + ) + ] + ) + + +def test_inline_image_without_data_is_rejected() -> None: + with pytest.raises(OpenRouterRequestError, match="Inline image data"): + contents_to_messages( + [ + _user( + types.Part( + inline_data=types.Blob( + data=None, + mime_type="image/png", + ) + ) + ) + ] + ) + + +def test_file_uri_content_is_rejected() -> None: + with pytest.raises(OpenRouterRequestError, match="File URI"): + contents_to_messages( + [ + _user( + types.Part( + file_data=types.FileData( + file_uri="gs://bucket/file.png", + mime_type="image/png", + ) + ) + ) + ] + ) + + +def test_unserializable_tool_response_is_rejected() -> None: + circular: dict[str, Any] = {} + circular["self"] = circular + + with pytest.raises(OpenRouterRequestError, match="could not be serialized"): + contents_to_messages( + [ + _model(_call()), + _user(_response(response=circular)), + ] + ) + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (None, None), + (" hello ", "hello"), + (types.Content(role="system", parts=[types.Part.from_text(text="hi")]), "hi"), + ([types.Part.from_text(text="a"), types.Part.from_text(text="b")], "ab"), + ], +) +def test_system_instruction_variants(value: object, expected: str | None) -> None: + assert _system_instruction_text(value) == expected + + +def test_system_instruction_rejects_media_and_unknown_types() -> None: + with pytest.raises(OpenRouterRequestError, match="Media parts"): + _system_instruction_text( + types.Content( + role="system", + parts=[types.Part.from_bytes(data=b"png", mime_type="image/png")], + ) + ) + with pytest.raises(OpenRouterRequestError, match="Unsupported system_instruction"): + _system_instruction_text(object()) + + +def test_build_chat_request_maps_supported_configuration() -> None: + tool = types.Tool( + function_declarations=[ + types.FunctionDeclaration( + name="lookup", + description="Look something up", + parameters_json_schema={ + "type": "object", + "properties": {"query": {"type": "string"}}, + "required": ["query"], + }, + ) + ] + ) + config = types.GenerateContentConfig( + system_instruction=types.Content( + role="system", + parts=[types.Part.from_text(text="Be concise")], + ), + temperature=0.2, + max_output_tokens=123, + top_p=0.9, + top_k=5, + stop_sequences=["END"], + presence_penalty=0.1, + frequency_penalty=0.2, + seed=7, + tools=[tool], + http_options=types.HttpOptions( + extra_body={ + "reasoning": {"effort": "high"}, + "model": "evil", + "top_k": 9, + }, + headers={"X-Test": "yes", "Authorization": "Bearer evil"}, + timeout=60_000, + retry_options=types.HttpRetryOptions(attempts=3), + ), + ) + + request = build_chat_request( + LlmRequest( + contents=[_user(types.Part.from_text(text="hello"))], + config=config, + ), + model="openrouter/google/gemini-2.5-flash", + ) + + assert request.model == "google/gemini-2.5-flash" + assert request.messages[0] == {"role": "system", "content": "Be concise"} + assert request.generation == { + "temperature": 0.2, + "max_tokens": 123, + "top_p": 0.9, + "stop": ["END"], + "presence_penalty": 0.1, + "frequency_penalty": 0.2, + "seed": 7, + } + assert request.tools is not None + assert request.tools[0]["function"]["name"] == "lookup" + assert request.extra_body == {"reasoning": {"effort": "high"}, "top_k": 5} + assert request.extra_headers == {"X-Test": "yes"} + assert request.timeout_seconds == 60.0 + assert request.max_retries == 2 + + kwargs = request.create_kwargs(stream=True) + assert kwargs["stream"] is True + assert kwargs["stream_options"] == {"include_usage": True} + assert kwargs["tool_choice"] == "auto" + assert kwargs["extra_body"] == {"reasoning": {"effort": "high"}, "top_k": 5} + + +@pytest.mark.parametrize( + "config", + [ + types.GenerateContentConfig(response_schema={"type": "object"}), + types.GenerateContentConfig(response_mime_type="application/json"), + types.GenerateContentConfig( + tool_config=types.ToolConfig( + function_calling_config=types.FunctionCallingConfig( + mode=types.FunctionCallingConfigMode.AUTO + ) + ) + ), + types.GenerateContentConfig( + safety_settings=[ + types.SafetySetting( + category=types.HarmCategory.HARM_CATEGORY_HATE_SPEECH, + threshold=types.HarmBlockThreshold.BLOCK_NONE, + ) + ] + ), + types.GenerateContentConfig( + thinking_config=types.ThinkingConfig(thinking_budget=0) + ), + types.GenerateContentConfig(candidate_count=2), + ], +) +def test_build_chat_request_rejects_unsupported_configuration( + config: types.GenerateContentConfig, +) -> None: + with pytest.raises(OpenRouterRequestError, match="does not support|only supports"): + build_chat_request( + LlmRequest(contents=[], config=config), + model="google/gemini-2.5-flash", + ) + + +def test_build_chat_request_rejects_invalid_http_options() -> None: + bad_extra_body = types.HttpOptions() + bad_extra_body.extra_body = cast(Any, ["not", "a", "mapping"]) + with pytest.raises(OpenRouterRequestError, match="extra_body must be a mapping"): + build_chat_request( + LlmRequest( + contents=[], + config=types.GenerateContentConfig(http_options=bad_extra_body), + ), + model="google/gemini-2.5-flash", + ) + + bad_headers = types.HttpOptions() + bad_headers.headers = cast(Any, ["not", "a", "mapping"]) + with pytest.raises(OpenRouterRequestError, match="headers must be a mapping"): + build_chat_request( + LlmRequest( + contents=[], + config=types.GenerateContentConfig(http_options=bad_headers), + ), + model="google/gemini-2.5-flash", + ) + + for http_options in ( + types.HttpOptions(timeout=0), + types.HttpOptions(retry_options=types.HttpRetryOptions(attempts=0)), + ): + with pytest.raises(OpenRouterRequestError, match="must be"): + build_chat_request( + LlmRequest( + contents=[], + config=types.GenerateContentConfig(http_options=http_options), + ), + model="google/gemini-2.5-flash", + ) + + +def test_tool_declarations_from_json_schema() -> None: + tools = _tool_declarations( + [ + types.Tool( + function_declarations=[ + types.FunctionDeclaration( + name="lookup", + description="d", + parameters_json_schema={ + "type": "object", + "properties": {"q": {"type": "string"}}, + }, + ) + ] + ) + ] + ) + + assert tools == [ + { + "type": "function", + "function": { + "name": "lookup", + "description": "d", + "parameters": { + "type": "object", + "properties": {"q": {"type": "string"}}, + }, + }, + } + ] + + +def test_tool_declarations_lowercase_legacy_schema_types() -> None: + tools = _tool_declarations( + [ + types.Tool( + function_declarations=[ + types.FunctionDeclaration( + name="lookup", + parameters=types.Schema( + type=types.Type.OBJECT, + properties={"query": types.Schema(type=types.Type.STRING)}, + ), + ) + ] + ) + ] + ) + + assert tools is not None + parameters = tools[0]["function"]["parameters"] + assert parameters["type"] == "object" + assert parameters["properties"]["query"]["type"] == "string" + + +def test_tool_declarations_rejects_builtin_and_non_tool_values() -> None: + with pytest.raises(OpenRouterRequestError, match="function tools only"): + _tool_declarations([types.Tool(google_search=types.GoogleSearch())]) + + with pytest.raises(OpenRouterRequestError, match="Unsupported tool type"): + _tool_declarations([object()]) + + +def test_tool_declarations_skips_empty_tools() -> None: + assert _tool_declarations([types.Tool()]) is None + assert _tool_declarations(None) is None + + +def test_chat_request_create_kwargs_omits_optional_fields() -> None: + request = ChatRequest(model="model", messages=[{"role": "user", "content": "hi"}]) + + kwargs = request.create_kwargs(stream=False) + + assert kwargs == { + "model": "model", + "messages": [{"role": "user", "content": "hi"}], + "stream": False, + } + + +def test_chat_request_create_kwargs_includes_tools_and_headers() -> None: + request = ChatRequest( + model="model", + messages=[], + tools=[{"type": "function", "function": {"name": "x"}}], + extra_headers={"X-Test": "yes"}, + ) + + kwargs = request.create_kwargs(stream=False) + + assert kwargs["tools"] == [{"type": "function", "function": {"name": "x"}}] + assert kwargs["tool_choice"] == "auto" + assert kwargs["extra_headers"] == {"X-Test": "yes"} + assert "stream_options" not in kwargs + + +def test_empty_model_part_is_skipped() -> None: + assert contents_to_messages([_model(types.Part())]) == [] + + +def test_user_thought_part_is_skipped() -> None: + assert contents_to_messages([_user(types.Part(text="thought", thought=True))]) == [] + + +def test_empty_user_part_is_skipped() -> None: + assert contents_to_messages([_user(types.Part())]) == [] + + +def test_system_instruction_thought_part_is_ignored() -> None: + assert ( + _system_instruction_text( + types.Content( + role="system", + parts=[types.Part(text="thought", thought=True)], + ) + ) + is None + ) + + +def test_string_tool_response_is_passed_through() -> None: + response = types.FunctionResponse( + name="lookup", + id="call-1", + response={"result": "ok"}, + ) + response.response = cast(Any, "plain text") + + messages = contents_to_messages( + [_model(_call()), _user(types.Part(function_response=response))] + ) + + assert messages[1]["content"] == "plain text" + + +def test_multiple_pending_calls_pair_by_name() -> None: + messages = contents_to_messages( + [ + _model( + _call(name="first", call_id="c1"), + _call(name="lookup", call_id="c2"), + ), + _user(_response(name="lookup", call_id=None)), + ] + ) + + assert messages[1]["tool_call_id"] == "c2" + + +def test_unmatched_tool_response_uses_first_pending_call() -> None: + messages = contents_to_messages( + [ + _model(_call(name="first", call_id="c1")), + _user(_response(name="other", call_id="unknown")), + ] + ) + + assert messages[1]["tool_call_id"] == "c1" + + +def test_orphan_tool_response_gets_fallback_id() -> None: + messages = contents_to_messages([_user(_response(name="lookup", call_id=None))]) + + assert messages[0]["tool_call_id"] == "blacki_tool_0" + + +def test_trailing_tool_call_gets_placeholder() -> None: + messages = contents_to_messages([_model(_call(call_id="call-1"))]) + + assert messages[-1]["role"] == "tool" + assert messages[-1]["tool_call_id"] == "call-1" + + +def test_ensure_tool_results_ignores_orphan_tool_ids() -> None: + from blacki.models.openrouter_request import _ensure_tool_results + + messages = _ensure_tool_results( + [{"role": "tool", "tool_call_id": "orphan", "content": "{}"}] + ) + + assert messages == [{"role": "tool", "tool_call_id": "orphan", "content": "{}"}] + + +def test_tool_declaration_without_name_is_rejected() -> None: + declaration = types.FunctionDeclaration(name="placeholder") + declaration.name = None + + with pytest.raises(OpenRouterRequestError, match="missing its name"): + _tool_declarations([types.Tool(function_declarations=[declaration])]) + + +def test_tool_declaration_without_parameters_gets_empty_schema() -> None: + tools = _tool_declarations( + [types.Tool(function_declarations=[types.FunctionDeclaration(name="x")])] + ) + + assert tools is not None + assert tools[0]["function"]["parameters"] == { + "type": "object", + "properties": {}, + } + + +def test_lowercase_schema_types_handles_lists_and_scalars() -> None: + from blacki.models.openrouter_request import ( + _lowercase_schema_types, + _normalize_schema_type, + ) + + assert _lowercase_schema_types( + {"type": "OBJECT", "enum": ["A", "B"], "description": "d"} + ) == {"type": "object", "enum": ["A", "B"], "description": "d"} + assert _normalize_schema_type("STRING") == "string" + assert _normalize_schema_type(5) == 5 + + +def test_circular_function_call_args_are_rejected() -> None: + circular: dict[str, Any] = {} + circular["self"] = circular + + with pytest.raises(OpenRouterRequestError, match="function-call argument"): + contents_to_messages([_model(_call(args=circular))]) diff --git a/tests/test_openrouter_response.py b/tests/test_openrouter_response.py new file mode 100644 index 0000000..a55e77e --- /dev/null +++ b/tests/test_openrouter_response.py @@ -0,0 +1,738 @@ +"""Tests for converting OpenRouter responses into ADK responses.""" + +from __future__ import annotations + +from typing import Any + +import pytest +from google.genai import types + +from blacki.models.openrouter_response import ( + OpenRouterResponseError, + OpenRouterStreamAccumulator, + _map_finish_reason, + _usage_metadata, + response_to_llm_response, +) + + +def _completion(**message: Any) -> dict[str, Any]: + return { + "id": "gen-1", + "model": "google/gemini-2.5-flash", + "choices": [ + { + "index": 0, + "message": {"role": "assistant", **message}, + "finish_reason": "stop", + } + ], + "usage": { + "prompt_tokens": 10, + "completion_tokens": 5, + "total_tokens": 15, + "cost": 0.01, + "cost_details": {"upstream_inference_cost": 0.009}, + "prompt_tokens_details": {"cached_tokens": 2}, + "completion_tokens_details": {"reasoning_tokens": 3}, + }, + } + + +def test_non_streaming_response_maps_text_reasoning_tools_and_usage() -> None: + payload = _completion( + content="hello", + reasoning="internal", + tool_calls=[ + { + "id": "call-1", + "type": "function", + "function": {"name": "lookup", "arguments": '{"query": "x"}'}, + } + ], + ) + payload["choices"][0]["finish_reason"] = "tool_calls" + + response = response_to_llm_response(payload) + + assert response.content is not None + assert response.content.parts is not None + assert [part.thought for part in response.content.parts] == [True, None, None] + assert response.content.parts[0].text == "internal" + assert response.content.parts[1].text == "hello" + call = response.content.parts[2].function_call + assert call is not None + assert call.id == "call-1" + assert call.name == "lookup" + assert call.args == {"query": "x"} + assert response.finish_reason == types.FinishReason.STOP + assert response.error_code is None + assert response.model_version == "google/gemini-2.5-flash" + assert response.usage_metadata is not None + assert response.usage_metadata.prompt_token_count == 10 + assert response.usage_metadata.candidates_token_count == 5 + assert response.usage_metadata.total_token_count == 15 + assert response.usage_metadata.cached_content_token_count == 2 + assert response.usage_metadata.thoughts_token_count == 3 + + +def test_non_streaming_content_list_and_reasoning_details() -> None: + payload = _completion( + content=[ + {"type": "text", "text": "a"}, + {"type": "image_url", "image_url": {"url": "ignored"}}, + "not-a-mapping", + {"type": "text", "text": 5}, + {"type": "text", "text": "b"}, + ], + reasoning_details=[ + {"type": "reasoning.text", "text": "one "}, + {"type": "reasoning.summary", "summary": "two"}, + {"type": "reasoning.encrypted", "data": "secret"}, + {"type": "reasoning.text", "text": ""}, + "not-a-mapping", + ], + ) + + response = response_to_llm_response(payload) + + assert response.content is not None + assert response.content.parts is not None + assert response.content.parts[0].text == "one two" + assert response.content.parts[0].thought is True + assert response.content.parts[1].text == "ab" + + +def test_non_streaming_missing_call_id_gets_blacki_id() -> None: + payload = _completion( + tool_calls=[ + {"type": "function", "function": {"name": "lookup", "arguments": "{}"}} + ] + ) + + response = response_to_llm_response(payload) + + assert response.content is not None + assert response.content.parts is not None + call = response.content.parts[0].function_call + assert call is not None + assert call.id is not None + assert call.id.startswith("blacki_call_") + + +@pytest.mark.parametrize( + ("finish_reason", "expected"), + [ + ("length", types.FinishReason.MAX_TOKENS), + ("content_filter", types.FinishReason.SAFETY), + ("mystery", types.FinishReason.OTHER), + ], +) +def test_non_streaming_non_stop_finish_reasons_set_errors( + finish_reason: str, + expected: types.FinishReason, +) -> None: + payload = _completion(content="text") + payload["choices"][0]["finish_reason"] = finish_reason + + response = response_to_llm_response(payload) + + assert response.finish_reason == expected + assert response.error_code == expected + assert response.error_message + + +def test_non_streaming_invalid_tool_json_raises() -> None: + payload = _completion( + tool_calls=[ + {"id": "1", "type": "function", "function": {"name": "x", "arguments": "{"}} + ] + ) + + with pytest.raises(OpenRouterResponseError, match="invalid JSON"): + response_to_llm_response(payload) + + +def test_non_streaming_truncated_tool_json_returns_max_tokens_response() -> None: + payload = _completion( + tool_calls=[ + {"id": "1", "type": "function", "function": {"name": "x", "arguments": "{"}} + ] + ) + payload["choices"][0]["finish_reason"] = "length" + + response = response_to_llm_response(payload) + + assert response.finish_reason == types.FinishReason.MAX_TOKENS + assert response.error_message is not None + assert "truncated" in response.error_message + assert response.content is not None + assert response.content.parts == [] + + +def test_non_streaming_empty_choices_returns_empty_content() -> None: + response = response_to_llm_response({"model": "m", "choices": []}) + + assert response.content is not None + assert response.content.parts == [] + assert response.model_version == "m" + + +@pytest.mark.parametrize( + "error", + [ + {"code": 402, "message": "Insufficient credits"}, + "provider disconnected", + ], +) +def test_non_streaming_error_payload_raises(error: object) -> None: + with pytest.raises(OpenRouterResponseError): + response_to_llm_response({"error": error}) + + +def test_non_streaming_finish_reason_error_raises() -> None: + payload = _completion(content="x") + payload["choices"][0]["finish_reason"] = "error" + + with pytest.raises(OpenRouterResponseError, match="finish_reason=error"): + response_to_llm_response(payload) + + +def test_non_streaming_invalid_choice_raises() -> None: + with pytest.raises(OpenRouterResponseError, match="invalid choice"): + response_to_llm_response({"choices": ["bad"]}) + + +def test_non_streaming_tool_call_shape_errors_raise() -> None: + with pytest.raises(OpenRouterResponseError, match="without a function"): + response_to_llm_response( + _completion(tool_calls=[{"id": "1", "type": "function"}]) + ) + + with pytest.raises(OpenRouterResponseError, match="without a name"): + response_to_llm_response( + _completion( + tool_calls=[{"id": "1", "type": "function", "function": {"name": None}}] + ) + ) + + +def test_non_streaming_non_mapping_tool_call_is_skipped() -> None: + response = response_to_llm_response(_completion(tool_calls=["bad"])) + + assert response.content is not None + assert response.content.parts == [] + + +@pytest.mark.parametrize( + "value", + [ + None, + {}, + {"prompt_tokens": "not-a-number"}, + {"prompt_tokens": -1}, + ], +) +def test_usage_metadata_handles_missing_or_invalid_values(value: object) -> None: + assert _usage_metadata(value) is None + + +def test_usage_metadata_handles_missing_details() -> None: + metadata = _usage_metadata( + { + "prompt_tokens": 1, + "completion_tokens": 2, + "total_tokens": 3, + "prompt_tokens_details": {"cached_tokens": "bad"}, + "completion_tokens_details": {"reasoning_tokens": 0}, + } + ) + + assert metadata is not None + assert metadata.cached_content_token_count is None + assert metadata.thoughts_token_count is None + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (None, None), + ("", None), + (types.FinishReason.SAFETY, types.FinishReason.SAFETY), + ("stop", types.FinishReason.STOP), + ("unknown", types.FinishReason.OTHER), + ], +) +def test_map_finish_reason(value: object, expected: types.FinishReason | None) -> None: + assert _map_finish_reason(value) == expected + + +def test_stream_accumulator_yields_partials_and_one_final_response() -> None: + accumulator = OpenRouterStreamAccumulator() + + partials = accumulator.add( + { + "model": "google/gemini-2.5-flash", + "choices": [ + { + "index": 0, + "delta": {"reasoning": "thinking"}, + "finish_reason": None, + } + ], + } + ) + assert len(partials) == 1 + assert partials[0].partial is True + assert partials[0].content is not None + assert partials[0].content.parts is not None + assert partials[0].content.parts[0].thought is True + + partials = accumulator.add( + { + "choices": [ + { + "index": 0, + "delta": {"content": "hello"}, + "finish_reason": None, + } + ] + } + ) + assert len(partials) == 1 + assert partials[0].content is not None + assert partials[0].content.parts is not None + assert partials[0].content.parts[0].text == "hello" + + partials = accumulator.add( + { + "choices": [ + { + "index": 0, + "delta": { + "tool_calls": [ + { + "index": 0, + "id": "call-1", + "function": { + "name": "lookup", + "arguments": '{"q":', + }, + } + ] + }, + "finish_reason": "tool_calls", + } + ] + } + ) + assert partials == [] + + partials = accumulator.add( + { + "choices": [ + { + "index": 0, + "delta": { + "tool_calls": [ + { + "index": 0, + "function": {"arguments": '"x"}'}, + } + ] + }, + "finish_reason": "tool_calls", + } + ], + "usage": { + "prompt_tokens": 4, + "completion_tokens": 2, + "total_tokens": 6, + "cost": 0.001, + }, + } + ) + assert partials == [] + + final = accumulator.finish() + assert final.partial is False + assert final.finish_reason == types.FinishReason.STOP + assert final.usage_metadata is not None + assert final.usage_metadata.total_token_count == 6 + assert final.content is not None + assert final.content.parts is not None + assert final.content.parts[0].text == "thinking" + assert final.content.parts[0].thought is True + assert final.content.parts[1].text == "hello" + call = final.content.parts[2].function_call + assert call is not None + assert call.id == "call-1" + assert call.args == {"q": "x"} + + +def test_stream_accumulator_usage_chunk_with_empty_choices() -> None: + accumulator = OpenRouterStreamAccumulator() + accumulator.add( + { + "choices": [], + "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}, + } + ) + + final = accumulator.finish() + + assert final.content is not None + assert final.content.parts == [] + assert final.usage_metadata is not None + assert final.finish_reason is None + + +def test_stream_accumulator_top_level_error_raises() -> None: + accumulator = OpenRouterStreamAccumulator() + + with pytest.raises(OpenRouterResponseError, match="Insufficient"): + accumulator.add({"error": {"code": 402, "message": "Insufficient credits"}}) + + +def test_stream_accumulator_finish_reason_error_raises() -> None: + accumulator = OpenRouterStreamAccumulator() + accumulator.add( + { + "choices": [ + { + "index": 0, + "delta": {"content": ""}, + "finish_reason": "error", + } + ] + } + ) + + with pytest.raises(OpenRouterResponseError, match="finish_reason=error"): + accumulator.finish() + + +def test_stream_accumulator_invalid_and_truncated_tool_json() -> None: + accumulator = OpenRouterStreamAccumulator() + accumulator.add( + { + "choices": [ + { + "index": 0, + "delta": { + "tool_calls": [ + { + "index": 0, + "id": "1", + "function": {"name": "x", "arguments": "{"}, + } + ] + }, + "finish_reason": "tool_calls", + } + ] + } + ) + with pytest.raises(OpenRouterResponseError, match="invalid JSON"): + accumulator.finish() + + truncated = OpenRouterStreamAccumulator() + truncated.add( + { + "choices": [ + { + "index": 0, + "delta": { + "tool_calls": [ + { + "index": 0, + "id": "1", + "function": {"name": "x", "arguments": "{"}, + } + ] + }, + "finish_reason": "length", + } + ] + } + ) + final = truncated.finish() + assert final.finish_reason == types.FinishReason.MAX_TOKENS + assert final.content is not None + assert final.content.parts == [] + + +def test_stream_accumulator_tool_arguments_without_name_raise() -> None: + accumulator = OpenRouterStreamAccumulator() + accumulator.add( + { + "choices": [ + { + "index": 0, + "delta": { + "tool_calls": [ + { + "index": 0, + "function": {"arguments": '{"q":"x"}'}, + } + ] + }, + "finish_reason": "tool_calls", + } + ] + } + ) + + with pytest.raises(OpenRouterResponseError, match="without a function name"): + accumulator.finish() + + +def test_stream_accumulator_skips_invalid_shapes() -> None: + accumulator = OpenRouterStreamAccumulator() + + assert accumulator.add({"choices": "not-a-list"}) == [] + assert accumulator.add({"choices": ["bad"]}) == [] + assert ( + accumulator.add( + { + "choices": [ + { + "index": 0, + "delta": "not-a-mapping", + "finish_reason": "stop", + } + ] + } + ) + == [] + ) + assert ( + accumulator.add( + { + "choices": [ + { + "index": 0, + "delta": {"tool_calls": ["bad", {"function": "bad"}]}, + "finish_reason": None, + } + ] + } + ) + == [] + ) + + final = accumulator.finish() + assert final.content is not None + assert final.content.parts == [] + + +def test_stream_accumulator_defaults_to_stop_and_deduplicates_repeated_names() -> None: + accumulator = OpenRouterStreamAccumulator() + accumulator.add( + { + "choices": [ + { + "index": 0, + "delta": { + "content": "answer", + "tool_calls": [ + { + "index": 0, + "function": {"name": "lookup", "arguments": ""}, + } + ], + }, + "finish_reason": None, + } + ] + } + ) + accumulator.add( + { + "choices": [ + { + "index": 0, + "delta": { + "tool_calls": [ + { + "index": 0, + "function": {"name": "lookup", "arguments": "{}"}, + } + ] + }, + "finish_reason": None, + } + ] + } + ) + + final = accumulator.finish() + + assert final.finish_reason == types.FinishReason.STOP + assert final.content is not None + assert final.content.parts is not None + call = final.content.parts[1].function_call + assert call is not None + assert call.name == "lookup" + assert call.id is not None + assert call.id.startswith("blacki_call_") + + +def test_stream_accumulator_ignores_empty_tool_call_entries() -> None: + accumulator = OpenRouterStreamAccumulator() + accumulator.add( + { + "choices": [ + { + "index": 0, + "delta": { + "tool_calls": [ + { + "index": 0, + "function": {"name": "", "arguments": ""}, + } + ] + }, + "finish_reason": "tool_calls", + } + ] + } + ) + + final = accumulator.finish() + + assert final.content is not None + assert final.content.parts == [] + + +def test_non_streaming_missing_message_becomes_empty_content() -> None: + response = response_to_llm_response( + {"choices": [{"message": "bad", "finish_reason": "stop"}]} + ) + + assert response.content is not None + assert response.content.parts == [] + + +@pytest.mark.parametrize( + "error", + [ + {"message": "oops"}, + {"code": 402}, + {}, + ], +) +def test_error_payload_variants_raise(error: object) -> None: + with pytest.raises(OpenRouterResponseError): + response_to_llm_response({"error": error}) + + +@pytest.mark.parametrize( + ("arguments", "expected"), + [ + (None, {}), + ({"q": "x"}, {"q": "x"}), + ("", {}), + ], +) +def test_tool_argument_shapes(arguments: object, expected: dict[str, Any]) -> None: + response = response_to_llm_response( + _completion( + tool_calls=[ + { + "id": "1", + "type": "function", + "function": {"name": "x", "arguments": arguments}, + } + ] + ) + ) + + assert response.content is not None + assert response.content.parts is not None + call = response.content.parts[0].function_call + assert call is not None + assert call.args == expected + + +@pytest.mark.parametrize("arguments", [5, "[1,2]"]) +def test_invalid_tool_argument_shapes_raise(arguments: object) -> None: + with pytest.raises(OpenRouterResponseError): + response_to_llm_response( + _completion( + tool_calls=[ + { + "id": "1", + "type": "function", + "function": {"name": "x", "arguments": arguments}, + } + ] + ) + ) + + +def test_usage_metadata_accepts_float_and_string_tokens() -> None: + metadata = _usage_metadata( + {"prompt_tokens": 2.5, "completion_tokens": "3", "total_tokens": 5} + ) + + assert metadata is not None + assert metadata.prompt_token_count == 2 + assert metadata.candidates_token_count == 3 + + +def test_usage_metadata_rejects_unsupported_types() -> None: + assert _usage_metadata({"prompt_tokens": []}) is None + + +def test_stream_accumulator_concatenates_name_fragments() -> None: + accumulator = OpenRouterStreamAccumulator() + accumulator.add( + { + "choices": [ + { + "index": 0, + "delta": { + "tool_calls": [ + { + "index": 0, + "id": "c1", + "function": {"name": "look", "arguments": "{"}, + } + ] + }, + "finish_reason": None, + } + ] + } + ) + accumulator.add( + { + "choices": [ + { + "index": 0, + "delta": { + "tool_calls": [ + { + "index": 0, + "function": { + "name": "up", + "arguments": '"q":"x"}', + }, + } + ] + }, + "finish_reason": "tool_calls", + } + ] + } + ) + + final = accumulator.finish() + + assert final.content is not None + assert final.content.parts is not None + call = final.content.parts[0].function_call + assert call is not None + assert call.name == "lookup" + assert call.args == {"q": "x"} diff --git a/uv.lock b/uv.lock index ba8a254..7604f1d 100644 --- a/uv.lock +++ b/uv.lock @@ -13,105 +13,6 @@ resolution-markers = [ "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", ] -[[package]] -name = "absl-py" -version = "2.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/4f/d79676ab82f2e42fc3611618139f13a9c4c31d0cff4b486982047679a802/absl_py-2.5.0.tar.gz", hash = "sha256:0c996f25c0490700fadabe6351630f6111534fa0ae252cc6d2014ea3b141135f", size = 118119, upload-time = "2026-07-03T10:57:48.157Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/0a/a10b45aab35b175aded078a462dc8d0c698f5b13946e7cb0869097b78bb6/absl_py-2.5.0-py3-none-any.whl", hash = "sha256:0f17b89f2a4eaaedc4f28c622998aa690564b3012a396a4ffad0821007fe03ba", size = 137410, upload-time = "2026-07-03T10:57:46.735Z" }, -] - -[[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" }, -] - -[[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 = "aiosqlite" version = "0.22.1" @@ -248,8 +149,8 @@ dependencies = [ { name = "google-cloud-logging" }, { name = "greenlet" }, { name = "httpx" }, - { name = "litellm" }, { name = "mem0ai" }, + { name = "openai" }, { name = "openinference-instrumentation-google-adk" }, { name = "opensandbox" }, { name = "opensandbox-code-interpreter" }, @@ -265,7 +166,6 @@ dependencies = [ [package.dev-dependencies] dev = [ - { name = "google-adk", extra = ["eval"] }, { name = "ipykernel" }, { name = "mypy" }, { name = "pytest" }, @@ -291,8 +191,8 @@ requires-dist = [ { name = "google-cloud-logging", specifier = ">=3.12.1,<4.0.0" }, { name = "greenlet", specifier = ">=3.0.0" }, { name = "httpx", specifier = ">=0.27.0,<1.0.0" }, - { name = "litellm", specifier = ">=1.60.0" }, { name = "mem0ai", specifier = ">=0.1.0,<1.0.0" }, + { name = "openai", specifier = ">=2.20,<3" }, { name = "openinference-instrumentation-google-adk", specifier = "==0.1.17" }, { name = "opensandbox", specifier = ">=0.1.7" }, { name = "opensandbox-code-interpreter", specifier = ">=0.1.2" }, @@ -308,7 +208,6 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ - { name = "google-adk", extras = ["eval"], specifier = "==2.5.0" }, { name = "ipykernel", specifier = ">=7.0.0,<8.0.0" }, { name = "mypy", specifier = ">=1.17.1,<2.0.0" }, { name = "pytest", specifier = ">=8.3.4,<9.0.0" }, @@ -650,15 +549,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, ] -[[package]] -name = "defusedxml" -version = "0.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, -] - [[package]] name = "distro" version = "1.9.0" @@ -668,15 +558,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, ] -[[package]] -name = "docstring-parser" -version = "0.18.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, -] - [[package]] name = "executing" version = "2.2.1" @@ -702,147 +583,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/c7/cb03251d9dfb177246a9809a76f189d21df32dbd4a845951881d11323b7f/fastapi-0.139.2-py3-none-any.whl", hash = "sha256:b9ad015a835173d59865e2f5d8296fbc2b317bf56a2ba1a5bfbdd03de2fd4b1c", size = 130234, upload-time = "2026-07-16T15:06:19.557Z" }, ] -[[package]] -name = "fastuuid" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/7d/d9daedf0f2ebcacd20d599928f8913e9d2aea1d56d2d355a93bfa2b611d7/fastuuid-0.14.0.tar.gz", hash = "sha256:178947fc2f995b38497a74172adee64fdeb8b7ec18f2a5934d037641ba265d26", size = 18232, upload-time = "2025-10-19T22:19:22.402Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/f3/12481bda4e5b6d3e698fbf525df4443cc7dce746f246b86b6fcb2fba1844/fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:73946cb950c8caf65127d4e9a325e2b6be0442a224fd51ba3b6ac44e1912ce34", size = 516386, upload-time = "2025-10-19T22:42:40.176Z" }, - { url = "https://files.pythonhosted.org/packages/59/19/2fc58a1446e4d72b655648eb0879b04e88ed6fa70d474efcf550f640f6ec/fastuuid-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:12ac85024637586a5b69645e7ed986f7535106ed3013640a393a03e461740cb7", size = 264569, upload-time = "2025-10-19T22:25:50.977Z" }, - { url = "https://files.pythonhosted.org/packages/78/29/3c74756e5b02c40cfcc8b1d8b5bac4edbd532b55917a6bcc9113550e99d1/fastuuid-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:05a8dde1f395e0c9b4be515b7a521403d1e8349443e7641761af07c7ad1624b1", size = 254366, upload-time = "2025-10-19T22:29:49.166Z" }, - { url = "https://files.pythonhosted.org/packages/52/96/d761da3fccfa84f0f353ce6e3eb8b7f76b3aa21fd25e1b00a19f9c80a063/fastuuid-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09378a05020e3e4883dfdab438926f31fea15fd17604908f3d39cbeb22a0b4dc", size = 278978, upload-time = "2025-10-19T22:35:41.306Z" }, - { url = "https://files.pythonhosted.org/packages/fc/c2/f84c90167cc7765cb82b3ff7808057608b21c14a38531845d933a4637307/fastuuid-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbb0c4b15d66b435d2538f3827f05e44e2baafcc003dd7d8472dc67807ab8fd8", size = 279692, upload-time = "2025-10-19T22:25:36.997Z" }, - { url = "https://files.pythonhosted.org/packages/af/7b/4bacd03897b88c12348e7bd77943bac32ccf80ff98100598fcff74f75f2e/fastuuid-0.14.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cd5a7f648d4365b41dbf0e38fe8da4884e57bed4e77c83598e076ac0c93995e7", size = 303384, upload-time = "2025-10-19T22:29:46.578Z" }, - { url = "https://files.pythonhosted.org/packages/c0/a2/584f2c29641df8bd810d00c1f21d408c12e9ad0c0dafdb8b7b29e5ddf787/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c0a94245afae4d7af8c43b3159d5e3934c53f47140be0be624b96acd672ceb73", size = 460921, upload-time = "2025-10-19T22:36:42.006Z" }, - { url = "https://files.pythonhosted.org/packages/24/68/c6b77443bb7764c760e211002c8638c0c7cce11cb584927e723215ba1398/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2b29e23c97e77c3a9514d70ce343571e469098ac7f5a269320a0f0b3e193ab36", size = 480575, upload-time = "2025-10-19T22:28:18.975Z" }, - { url = "https://files.pythonhosted.org/packages/5a/87/93f553111b33f9bb83145be12868c3c475bf8ea87c107063d01377cc0e8e/fastuuid-0.14.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1e690d48f923c253f28151b3a6b4e335f2b06bf669c68a02665bc150b7839e94", size = 452317, upload-time = "2025-10-19T22:25:32.75Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8c/a04d486ca55b5abb7eaa65b39df8d891b7b1635b22db2163734dc273579a/fastuuid-0.14.0-cp311-cp311-win32.whl", hash = "sha256:a6f46790d59ab38c6aa0e35c681c0484b50dc0acf9e2679c005d61e019313c24", size = 154804, upload-time = "2025-10-19T22:24:15.615Z" }, - { url = "https://files.pythonhosted.org/packages/9c/b2/2d40bf00820de94b9280366a122cbaa60090c8cf59e89ac3938cf5d75895/fastuuid-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:e150eab56c95dc9e3fefc234a0eedb342fac433dacc273cd4d150a5b0871e1fa", size = 156099, upload-time = "2025-10-19T22:24:31.646Z" }, - { url = "https://files.pythonhosted.org/packages/02/a2/e78fcc5df65467f0d207661b7ef86c5b7ac62eea337c0c0fcedbeee6fb13/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77e94728324b63660ebf8adb27055e92d2e4611645bf12ed9d88d30486471d0a", size = 510164, upload-time = "2025-10-19T22:31:45.635Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b3/c846f933f22f581f558ee63f81f29fa924acd971ce903dab1a9b6701816e/fastuuid-0.14.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:caa1f14d2102cb8d353096bc6ef6c13b2c81f347e6ab9d6fbd48b9dea41c153d", size = 261837, upload-time = "2025-10-19T22:38:38.53Z" }, - { url = "https://files.pythonhosted.org/packages/54/ea/682551030f8c4fa9a769d9825570ad28c0c71e30cf34020b85c1f7ee7382/fastuuid-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d23ef06f9e67163be38cece704170486715b177f6baae338110983f99a72c070", size = 251370, upload-time = "2025-10-19T22:40:26.07Z" }, - { url = "https://files.pythonhosted.org/packages/14/dd/5927f0a523d8e6a76b70968e6004966ee7df30322f5fc9b6cdfb0276646a/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c9ec605ace243b6dbe3bd27ebdd5d33b00d8d1d3f580b39fdd15cd96fd71796", size = 277766, upload-time = "2025-10-19T22:37:23.779Z" }, - { url = "https://files.pythonhosted.org/packages/16/6e/c0fb547eef61293153348f12e0f75a06abb322664b34a1573a7760501336/fastuuid-0.14.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:808527f2407f58a76c916d6aa15d58692a4a019fdf8d4c32ac7ff303b7d7af09", size = 278105, upload-time = "2025-10-19T22:26:56.821Z" }, - { url = "https://files.pythonhosted.org/packages/2d/b1/b9c75e03b768f61cf2e84ee193dc18601aeaf89a4684b20f2f0e9f52b62c/fastuuid-0.14.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fb3c0d7fef6674bbeacdd6dbd386924a7b60b26de849266d1ff6602937675c8", size = 301564, upload-time = "2025-10-19T22:30:31.604Z" }, - { url = "https://files.pythonhosted.org/packages/fc/fa/f7395fdac07c7a54f18f801744573707321ca0cee082e638e36452355a9d/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab3f5d36e4393e628a4df337c2c039069344db5f4b9d2a3c9cea48284f1dd741", size = 459659, upload-time = "2025-10-19T22:31:32.341Z" }, - { url = "https://files.pythonhosted.org/packages/66/49/c9fd06a4a0b1f0f048aacb6599e7d96e5d6bc6fa680ed0d46bf111929d1b/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:b9a0ca4f03b7e0b01425281ffd44e99d360e15c895f1907ca105854ed85e2057", size = 478430, upload-time = "2025-10-19T22:26:22.962Z" }, - { url = "https://files.pythonhosted.org/packages/be/9c/909e8c95b494e8e140e8be6165d5fc3f61fdc46198c1554df7b3e1764471/fastuuid-0.14.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3acdf655684cc09e60fb7e4cf524e8f42ea760031945aa8086c7eae2eeeabeb8", size = 450894, upload-time = "2025-10-19T22:27:01.647Z" }, - { url = "https://files.pythonhosted.org/packages/90/eb/d29d17521976e673c55ef7f210d4cdd72091a9ec6755d0fd4710d9b3c871/fastuuid-0.14.0-cp312-cp312-win32.whl", hash = "sha256:9579618be6280700ae36ac42c3efd157049fe4dd40ca49b021280481c78c3176", size = 154374, upload-time = "2025-10-19T22:29:19.879Z" }, - { url = "https://files.pythonhosted.org/packages/cc/fc/f5c799a6ea6d877faec0472d0b27c079b47c86b1cdc577720a5386483b36/fastuuid-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:d9e4332dc4ba054434a9594cbfaf7823b57993d7d8e7267831c3e059857cf397", size = 156550, upload-time = "2025-10-19T22:27:49.658Z" }, - { url = "https://files.pythonhosted.org/packages/a5/83/ae12dd39b9a39b55d7f90abb8971f1a5f3c321fd72d5aa83f90dc67fe9ed/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77a09cb7427e7af74c594e409f7731a0cf887221de2f698e1ca0ebf0f3139021", size = 510720, upload-time = "2025-10-19T22:42:34.633Z" }, - { url = "https://files.pythonhosted.org/packages/53/b0/a4b03ff5d00f563cc7546b933c28cb3f2a07344b2aec5834e874f7d44143/fastuuid-0.14.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9bd57289daf7b153bfa3e8013446aa144ce5e8c825e9e366d455155ede5ea2dc", size = 262024, upload-time = "2025-10-19T22:30:25.482Z" }, - { url = "https://files.pythonhosted.org/packages/9c/6d/64aee0a0f6a58eeabadd582e55d0d7d70258ffdd01d093b30c53d668303b/fastuuid-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ac60fc860cdf3c3f327374db87ab8e064c86566ca8c49d2e30df15eda1b0c2d5", size = 251679, upload-time = "2025-10-19T22:36:14.096Z" }, - { url = "https://files.pythonhosted.org/packages/60/f5/a7e9cda8369e4f7919d36552db9b2ae21db7915083bc6336f1b0082c8b2e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab32f74bd56565b186f036e33129da77db8be09178cd2f5206a5d4035fb2a23f", size = 277862, upload-time = "2025-10-19T22:36:23.302Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d3/8ce11827c783affffd5bd4d6378b28eb6cc6d2ddf41474006b8d62e7448e/fastuuid-0.14.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33e678459cf4addaedd9936bbb038e35b3f6b2061330fd8f2f6a1d80414c0f87", size = 278278, upload-time = "2025-10-19T22:29:43.809Z" }, - { url = "https://files.pythonhosted.org/packages/a2/51/680fb6352d0bbade04036da46264a8001f74b7484e2fd1f4da9e3db1c666/fastuuid-0.14.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e3cc56742f76cd25ecb98e4b82a25f978ccffba02e4bdce8aba857b6d85d87b", size = 301788, upload-time = "2025-10-19T22:36:06.825Z" }, - { url = "https://files.pythonhosted.org/packages/fa/7c/2014b5785bd8ebdab04ec857635ebd84d5ee4950186a577db9eff0fb8ff6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cb9a030f609194b679e1660f7e32733b7a0f332d519c5d5a6a0a580991290022", size = 459819, upload-time = "2025-10-19T22:35:31.623Z" }, - { url = "https://files.pythonhosted.org/packages/01/d2/524d4ceeba9160e7a9bc2ea3e8f4ccf1ad78f3bde34090ca0c51f09a5e91/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:09098762aad4f8da3a888eb9ae01c84430c907a297b97166b8abc07b640f2995", size = 478546, upload-time = "2025-10-19T22:26:03.023Z" }, - { url = "https://files.pythonhosted.org/packages/bc/17/354d04951ce114bf4afc78e27a18cfbd6ee319ab1829c2d5fb5e94063ac6/fastuuid-0.14.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1383fff584fa249b16329a059c68ad45d030d5a4b70fb7c73a08d98fd53bcdab", size = 450921, upload-time = "2025-10-19T22:31:02.151Z" }, - { url = "https://files.pythonhosted.org/packages/fb/be/d7be8670151d16d88f15bb121c5b66cdb5ea6a0c2a362d0dcf30276ade53/fastuuid-0.14.0-cp313-cp313-win32.whl", hash = "sha256:a0809f8cc5731c066c909047f9a314d5f536c871a7a22e815cc4967c110ac9ad", size = 154559, upload-time = "2025-10-19T22:36:36.011Z" }, - { url = "https://files.pythonhosted.org/packages/22/1d/5573ef3624ceb7abf4a46073d3554e37191c868abc3aecd5289a72f9810a/fastuuid-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:0df14e92e7ad3276327631c9e7cec09e32572ce82089c55cb1bb8df71cf394ed", size = 156539, upload-time = "2025-10-19T22:33:35.898Z" }, -] - -[[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/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 = "fsspec" -version = "2026.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d5/7d/5df2650c57d47c57232af5ef4b4fdbff182070421e405e0d62c6cdbfaa87/fsspec-2026.1.0.tar.gz", hash = "sha256:e987cb0496a0d81bba3a9d1cee62922fb395e7d4c3b575e57f547953334fe07b", size = 310496, upload-time = "2026-01-09T15:21:35.562Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/01/c9/97cc5aae1648dcb851958a3ddf73ccd7dbe5650d95203ecb4d7720b4cdbf/fsspec-2026.1.0-py3-none-any.whl", hash = "sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc", size = 201838, upload-time = "2026-01-09T15:21:34.041Z" }, -] - -[[package]] -name = "gepa" -version = "0.1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/95/56/925779e5690971f1b022f7d107caf015c33ec09560261273ec137e23a8f2/gepa-0.1.4.tar.gz", hash = "sha256:6dd153a676ae5481764860d19286a9c0e8ddb5ef70d7f13044faf24978bdb6b8", size = 351343, upload-time = "2026-07-15T14:53:59.929Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fd/77/5b3a281cfd9caaa9e68349b434cf27f1ca448003ee0067a1ae2184dc52d1/gepa-0.1.4-py3-none-any.whl", hash = "sha256:12b971039599625c156d2231f6d72a29c31a22e9c237689459b5f1a3c353f532", size = 290167, upload-time = "2026-07-15T14:53:58.422Z" }, -] - [[package]] name = "ghp-import" version = "2.1.0" @@ -891,14 +631,6 @@ wheels = [ ] [package.optional-dependencies] -eval = [ - { name = "gepa" }, - { name = "google-cloud-aiplatform", extra = ["evaluation"] }, - { name = "jinja2" }, - { name = "pandas" }, - { name = "rouge-score" }, - { name = "tabulate" }, -] mcp = [ { name = "anyio" }, { name = "mcp" }, @@ -947,41 +679,6 @@ requests = [ { name = "requests" }, ] -[[package]] -name = "google-cloud-aiplatform" -version = "1.162.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "docstring-parser" }, - { name = "google-api-core", extra = ["grpc"] }, - { name = "google-auth" }, - { name = "google-cloud-bigquery" }, - { name = "google-cloud-resource-manager" }, - { name = "google-cloud-storage" }, - { name = "google-genai" }, - { name = "packaging" }, - { name = "proto-plus" }, - { name = "protobuf" }, - { name = "pydantic" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ba/fa/20a86701f1d3bcbedfe03a965bd4ae000ba79863fb1c2b4b658965f1057d/google_cloud_aiplatform-1.162.0.tar.gz", hash = "sha256:2403f75e6e44e879aecc0eab2d8c26e972507e4b4414d873b31cd4f4b49c9edb", size = 11218765, upload-time = "2026-07-21T23:17:44.419Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/b8/6f10db46713d03d4e889d569b8b0e10e4334d2cab26b9f865c78b1e4e4d9/google_cloud_aiplatform-1.162.0-py2.py3-none-any.whl", hash = "sha256:07ac79932b8f7a683c3bec6dcc21dd05234b55d01636b5a622ec93c18aad15fd", size = 9391054, upload-time = "2026-07-21T23:17:41.173Z" }, -] - -[package.optional-dependencies] -evaluation = [ - { name = "jsonschema" }, - { name = "litellm" }, - { name = "pandas" }, - { name = "pyyaml" }, - { name = "ruamel-yaml" }, - { name = "scikit-learn" }, - { name = "tqdm" }, -] - [[package]] name = "google-cloud-appengine-logging" version = "1.8.0" @@ -1011,24 +708,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9b/25/532886995f11102ad6de290496de5db227bd3a73827702445928ad32edcb/google_cloud_audit_log-0.4.0-py3-none-any.whl", hash = "sha256:6b88e2349df45f8f4cc0993b687109b1388da1571c502dc1417efa4b66ec55e0", size = 44890, upload-time = "2025-10-17T02:30:55.11Z" }, ] -[[package]] -name = "google-cloud-bigquery" -version = "3.42.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-api-core", extra = ["grpc"] }, - { name = "google-auth", extra = ["pyopenssl"] }, - { name = "google-cloud-core" }, - { name = "google-resumable-media" }, - { name = "packaging" }, - { name = "python-dateutil" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a0/7a/6109aa1803b0c8c93c7064e0e555cff1b12e1692b7b7bc63cdc1619e3722/google_cloud_bigquery-3.42.2.tar.gz", hash = "sha256:08d4b264e5ee4790f719724c76b538f204b7190999328a2f1a6a95eaab74ca39", size = 517250, upload-time = "2026-07-08T17:03:40.675Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/ee/3f3ff62d4ce39e6868ef9b98bea0af46f0c9c270092ef64d2bb5897c6e11/google_cloud_bigquery-3.42.2-py3-none-any.whl", hash = "sha256:41658c19e8ed5b83307011b4e55aca3b1f72052545a22788f1d637984615173f", size = 264272, upload-time = "2026-07-08T17:03:09.511Z" }, -] - [[package]] name = "google-cloud-core" version = "2.5.0" @@ -1062,65 +741,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/5a/778dca2e375171af4085554cb3bc643627717a7e4e1539842ced3afd6ec4/google_cloud_logging-3.13.0-py3-none-any.whl", hash = "sha256:f215e1c76ee29239c6cacf02443dffa985663c74bf47c9818854694805c6019f", size = 230518, upload-time = "2025-12-16T14:11:05.894Z" }, ] -[[package]] -name = "google-cloud-resource-manager" -version = "1.18.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-api-core", extra = ["grpc"] }, - { name = "google-auth" }, - { name = "grpc-google-iam-v1" }, - { name = "grpcio" }, - { name = "proto-plus" }, - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/12/70/d467d93528905700826d26a48ad16ac39d4b36dd6efb5a86fb338efa81aa/google_cloud_resource_manager-1.18.0.tar.gz", hash = "sha256:db689f800a14c66d041196a7fbb8bb8aae8dc87f28c2929e101a5ec766b15512", size = 463674, upload-time = "2026-06-22T23:22:27.09Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/62/2dbecda08a32aa9d4e6909eba3e31731c2d4452687bb6364a8d7950aed74/google_cloud_resource_manager-1.18.0-py3-none-any.whl", hash = "sha256:69bab144acd75878ebe44b720903dc3d140cb7d3be3261eaa8bc81e48afaff33", size = 404211, upload-time = "2026-06-22T23:20:25.88Z" }, -] - -[[package]] -name = "google-cloud-storage" -version = "3.13.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-api-core" }, - { name = "google-auth" }, - { name = "google-cloud-core" }, - { name = "google-crc32c" }, - { name = "google-resumable-media" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e3/25/355ed97c1723c787dfaa888808d55db18371f82c38ff862357b1e902cd19/google_cloud_storage-3.13.0.tar.gz", hash = "sha256:d11d8706ea1520fba0f21043bcb7897caf7015d76ce1ad9a4f60237e4d7a9f6c", size = 17340960, upload-time = "2026-07-13T19:10:07.524Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/e8/b3678a0931ee7d4b3fdaf0813e6206d66e0922b2c26d912f308728b5b95a/google_cloud_storage-3.13.0-py3-none-any.whl", hash = "sha256:648af3ef8a6acc674e1359d3c920c67eb89a7a5ab66b336bd3ac43fed6b5ab84", size = 341428, upload-time = "2026-07-13T19:09:52.39Z" }, -] - -[[package]] -name = "google-crc32c" -version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79", size = 14192, upload-time = "2025-12-16T00:35:25.142Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/ef/21ccfaab3d5078d41efe8612e0ed0bfc9ce22475de074162a91a25f7980d/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8", size = 31298, upload-time = "2025-12-16T00:20:32.241Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b8/f8413d3f4b676136e965e764ceedec904fe38ae8de0cdc52a12d8eb1096e/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7", size = 30872, upload-time = "2025-12-16T00:33:58.785Z" }, - { url = "https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15", size = 33243, upload-time = "2025-12-16T00:40:21.46Z" }, - { url = "https://files.pythonhosted.org/packages/71/03/4820b3bd99c9653d1a5210cb32f9ba4da9681619b4d35b6a052432df4773/google_crc32c-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a", size = 33608, upload-time = "2025-12-16T00:40:22.204Z" }, - { url = "https://files.pythonhosted.org/packages/7c/43/acf61476a11437bf9733fb2f70599b1ced11ec7ed9ea760fdd9a77d0c619/google_crc32c-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2", size = 34439, upload-time = "2025-12-16T00:35:20.458Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5f/7307325b1198b59324c0fa9807cafb551afb65e831699f2ce211ad5c8240/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113", size = 31300, upload-time = "2025-12-16T00:21:56.723Z" }, - { url = "https://files.pythonhosted.org/packages/21/8e/58c0d5d86e2220e6a37befe7e6a94dd2f6006044b1a33edf1ff6d9f7e319/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb", size = 30867, upload-time = "2025-12-16T00:38:31.302Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a9/a780cc66f86335a6019f557a8aaca8fbb970728f0efd2430d15ff1beae0e/google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", size = 33364, upload-time = "2025-12-16T00:40:22.96Z" }, - { url = "https://files.pythonhosted.org/packages/21/3f/3457ea803db0198c9aaca2dd373750972ce28a26f00544b6b85088811939/google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454", size = 33740, upload-time = "2025-12-16T00:40:23.96Z" }, - { url = "https://files.pythonhosted.org/packages/df/c0/87c2073e0c72515bb8733d4eef7b21548e8d189f094b5dad20b0ecaf64f6/google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962", size = 34437, upload-time = "2025-12-16T00:35:21.395Z" }, - { url = "https://files.pythonhosted.org/packages/d1/db/000f15b41724589b0e7bc24bc7a8967898d8d3bc8caf64c513d91ef1f6c0/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b", size = 31297, upload-time = "2025-12-16T00:23:20.709Z" }, - { url = "https://files.pythonhosted.org/packages/d7/0d/8ebed0c39c53a7e838e2a486da8abb0e52de135f1b376ae2f0b160eb4c1a/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27", size = 30867, upload-time = "2025-12-16T00:43:14.628Z" }, - { url = "https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa", size = 33344, upload-time = "2025-12-16T00:40:24.742Z" }, - { url = "https://files.pythonhosted.org/packages/1c/e8/b33784d6fc77fb5062a8a7854e43e1e618b87d5ddf610a88025e4de6226e/google_crc32c-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8", size = 33694, upload-time = "2025-12-16T00:40:25.505Z" }, - { url = "https://files.pythonhosted.org/packages/92/b1/d3cbd4d988afb3d8e4db94ca953df429ed6db7282ed0e700d25e6c7bfc8d/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f", size = 34435, upload-time = "2025-12-16T00:35:22.107Z" }, - { url = "https://files.pythonhosted.org/packages/52/c5/c171e4d8c44fec1422d801a6d2e5d7ddabd733eeda505c79730ee9607f07/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93", size = 28615, upload-time = "2025-12-16T00:40:29.298Z" }, - { url = "https://files.pythonhosted.org/packages/9c/97/7d75fe37a7a6ed171a2cf17117177e7aab7e6e0d115858741b41e9dd4254/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c", size = 28800, upload-time = "2025-12-16T00:40:30.322Z" }, -] - [[package]] name = "google-genai" version = "2.14.0" @@ -1142,18 +762,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ba/86/5ac5fb53e44cca4a6607fb917eb331fa237c65a103b9ec2e8e8acc8a42db/google_genai-2.14.0-py3-none-any.whl", hash = "sha256:ae7172cdd35695189b516b33a878e4132e5daa2dbc03a5b44cddfa8a82fad664", size = 1030738, upload-time = "2026-07-22T21:35:42.785Z" }, ] -[[package]] -name = "google-resumable-media" -version = "2.10.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-crc32c" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/48/f8/1ca5781d6be9cb9f73f7d40f4958c4bd1226a60598e3e39e1d6aaf838c4b/google_resumable_media-2.10.0.tar.gz", hash = "sha256:e324bc9d0fdae4c52a08ae90456edc4e71ece858399e1217ac0eb3a51d6bc6ee", size = 2164570, upload-time = "2026-06-03T16:14:26.103Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b0/d8/00c6854ac1512bb9eaf13bd3f8f28222f7674947fc510a4ff7616f2efc80/google_resumable_media-2.10.0-py3-none-any.whl", hash = "sha256:88152884bee37b2bf36a0ab81ad8c7fd12212c9803dd981d77c1b35b02d34e7c", size = 81533, upload-time = "2026-06-03T16:13:12.51Z" }, -] - [[package]] name = "googleapis-common-protos" version = "1.72.0" @@ -1306,28 +914,6 @@ 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 = "hf-xet" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870, upload-time = "2025-10-24T19:04:11.422Z" }, - { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584, upload-time = "2025-10-24T19:04:09.586Z" }, - { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004, upload-time = "2025-10-24T19:04:00.314Z" }, - { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636, upload-time = "2025-10-24T19:03:58.111Z" }, - { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" }, - { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866, upload-time = "2025-10-24T19:04:33.461Z" }, - { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, - { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, - { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, - { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" }, - { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" }, - { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" }, - { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" }, -] - [[package]] name = "hpack" version = "4.1.0" @@ -1379,27 +965,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, ] -[[package]] -name = "huggingface-hub" -version = "1.3.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "filelock" }, - { name = "fsspec" }, - { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, - { name = "httpx" }, - { name = "packaging" }, - { name = "pyyaml" }, - { name = "shellingham" }, - { name = "tqdm" }, - { name = "typer-slim" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/02/c3/544cd4cdd4b3c6de8591b56bb69efc3682e9ac81e36135c02e909dd98c5b/huggingface_hub-1.3.3.tar.gz", hash = "sha256:f8be6f468da4470db48351e8c77d6d8115dff9b3daeb30276e568767b1ff7574", size = 627649, upload-time = "2026-01-22T13:59:46.931Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/e8/0d032698916b9773b710c46e3b8e0154fc34cd017b151cc316c84c6c34fe/huggingface_hub-1.3.3-py3-none-any.whl", hash = "sha256:44af7b62380efc87c1c3bde7e1bf0661899b5bdfca1fc60975c61ee68410e10e", size = 536604, upload-time = "2026-01-22T13:59:45.391Z" }, -] - [[package]] name = "hyperframe" version = "6.1.0" @@ -1418,18 +983,6 @@ 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 = "importlib-metadata" -version = "8.7.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, -] - [[package]] name = "iniconfig" version = "2.3.0" @@ -1590,15 +1143,6 @@ 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 = "joblib" -version = "1.5.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, -] - [[package]] name = "jsonschema" version = "4.26.0" @@ -1696,29 +1240,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" }, ] -[[package]] -name = "litellm" -version = "1.85.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, - { name = "click" }, - { name = "fastuuid" }, - { name = "httpx" }, - { name = "importlib-metadata" }, - { name = "jinja2" }, - { name = "jsonschema" }, - { name = "openai" }, - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "tiktoken" }, - { name = "tokenizers" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0d/59/364fa090acef716effd88e6432d3442982b1f965b2d291f1528db0e6aabb/litellm-1.85.7.tar.gz", hash = "sha256:461cc5d5a6c7d5086b67f06f1d5a00c40d6fb0240099764cb040dc4a28452938", size = 15363830, upload-time = "2026-06-24T04:54:55.115Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bd/3b/baf1f8736c572f308eea14100372c39b3f70ee540e8cda537edd594628fc/litellm-1.85.7-py3-none-any.whl", hash = "sha256:4e07ec1c22aa81897daed83d7e98eb05187a82827918c240e2952384780f9c62", size = 16996595, upload-time = "2026-06-24T04:54:51.974Z" }, -] - [[package]] name = "markdown" version = "3.10.2" @@ -1913,87 +1434,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, ] -[[package]] -name = "multidict" -version = "6.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/34/9e/5c727587644d67b2ed479041e4b1c58e30afc011e3d45d25bbe35781217c/multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", size = 76604, upload-time = "2025-10-06T14:48:54.277Z" }, - { url = "https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", size = 44715, upload-time = "2025-10-06T14:48:55.445Z" }, - { url = "https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", size = 44332, upload-time = "2025-10-06T14:48:56.706Z" }, - { url = "https://files.pythonhosted.org/packages/31/61/0c2d50241ada71ff61a79518db85ada85fdabfcf395d5968dae1cbda04e5/multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", size = 245212, upload-time = "2025-10-06T14:48:58.042Z" }, - { url = "https://files.pythonhosted.org/packages/ac/e0/919666a4e4b57fff1b57f279be1c9316e6cdc5de8a8b525d76f6598fefc7/multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", size = 246671, upload-time = "2025-10-06T14:49:00.004Z" }, - { url = "https://files.pythonhosted.org/packages/a1/cc/d027d9c5a520f3321b65adea289b965e7bcbd2c34402663f482648c716ce/multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", size = 225491, upload-time = "2025-10-06T14:49:01.393Z" }, - { url = "https://files.pythonhosted.org/packages/75/c4/bbd633980ce6155a28ff04e6a6492dd3335858394d7bb752d8b108708558/multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", size = 257322, upload-time = "2025-10-06T14:49:02.745Z" }, - { url = "https://files.pythonhosted.org/packages/4c/6d/d622322d344f1f053eae47e033b0b3f965af01212de21b10bcf91be991fb/multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", size = 254694, upload-time = "2025-10-06T14:49:04.15Z" }, - { url = "https://files.pythonhosted.org/packages/a8/9f/78f8761c2705d4c6d7516faed63c0ebdac569f6db1bef95e0d5218fdc146/multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", size = 246715, upload-time = "2025-10-06T14:49:05.967Z" }, - { url = "https://files.pythonhosted.org/packages/78/59/950818e04f91b9c2b95aab3d923d9eabd01689d0dcd889563988e9ea0fd8/multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", size = 243189, upload-time = "2025-10-06T14:49:07.37Z" }, - { url = "https://files.pythonhosted.org/packages/7a/3d/77c79e1934cad2ee74991840f8a0110966d9599b3af95964c0cd79bb905b/multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", size = 237845, upload-time = "2025-10-06T14:49:08.759Z" }, - { url = "https://files.pythonhosted.org/packages/63/1b/834ce32a0a97a3b70f86437f685f880136677ac00d8bce0027e9fd9c2db7/multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", size = 246374, upload-time = "2025-10-06T14:49:10.574Z" }, - { url = "https://files.pythonhosted.org/packages/23/ef/43d1c3ba205b5dec93dc97f3fba179dfa47910fc73aaaea4f7ceb41cec2a/multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", size = 253345, upload-time = "2025-10-06T14:49:12.331Z" }, - { url = "https://files.pythonhosted.org/packages/6b/03/eaf95bcc2d19ead522001f6a650ef32811aa9e3624ff0ad37c445c7a588c/multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", size = 246940, upload-time = "2025-10-06T14:49:13.821Z" }, - { url = "https://files.pythonhosted.org/packages/e8/df/ec8a5fd66ea6cd6f525b1fcbb23511b033c3e9bc42b81384834ffa484a62/multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", size = 242229, upload-time = "2025-10-06T14:49:15.603Z" }, - { url = "https://files.pythonhosted.org/packages/8a/a2/59b405d59fd39ec86d1142630e9049243015a5f5291ba49cadf3c090c541/multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", size = 41308, upload-time = "2025-10-06T14:49:16.871Z" }, - { url = "https://files.pythonhosted.org/packages/32/0f/13228f26f8b882c34da36efa776c3b7348455ec383bab4a66390e42963ae/multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", size = 46037, upload-time = "2025-10-06T14:49:18.457Z" }, - { url = "https://files.pythonhosted.org/packages/84/1f/68588e31b000535a3207fd3c909ebeec4fb36b52c442107499c18a896a2a/multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", size = 43023, upload-time = "2025-10-06T14:49:19.648Z" }, - { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" }, - { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" }, - { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" }, - { url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545, upload-time = "2025-10-06T14:49:24.882Z" }, - { url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305, upload-time = "2025-10-06T14:49:26.778Z" }, - { url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363, upload-time = "2025-10-06T14:49:28.562Z" }, - { url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375, upload-time = "2025-10-06T14:49:29.96Z" }, - { url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346, upload-time = "2025-10-06T14:49:31.404Z" }, - { url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107, upload-time = "2025-10-06T14:49:32.974Z" }, - { url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592, upload-time = "2025-10-06T14:49:34.52Z" }, - { url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024, upload-time = "2025-10-06T14:49:35.956Z" }, - { url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484, upload-time = "2025-10-06T14:49:37.631Z" }, - { url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" }, - { url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" }, - { url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" }, - { url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" }, - { url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" }, - { url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" }, - { url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" }, - { url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" }, - { url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" }, - { url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342, upload-time = "2025-10-06T14:49:58.368Z" }, - { url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082, upload-time = "2025-10-06T14:49:59.89Z" }, - { url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704, upload-time = "2025-10-06T14:50:01.485Z" }, - { url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355, upload-time = "2025-10-06T14:50:02.955Z" }, - { url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259, upload-time = "2025-10-06T14:50:04.446Z" }, - { url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903, upload-time = "2025-10-06T14:50:05.98Z" }, - { url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365, upload-time = "2025-10-06T14:50:07.511Z" }, - { url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062, upload-time = "2025-10-06T14:50:09.074Z" }, - { url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683, upload-time = "2025-10-06T14:50:10.714Z" }, - { url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254, upload-time = "2025-10-06T14:50:12.28Z" }, - { url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967, upload-time = "2025-10-06T14:50:14.16Z" }, - { url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085, upload-time = "2025-10-06T14:50:15.639Z" }, - { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713, upload-time = "2025-10-06T14:50:17.066Z" }, - { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915, upload-time = "2025-10-06T14:50:18.264Z" }, - { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077, upload-time = "2025-10-06T14:50:19.853Z" }, - { url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114, upload-time = "2025-10-06T14:50:21.223Z" }, - { url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442, upload-time = "2025-10-06T14:50:22.871Z" }, - { url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885, upload-time = "2025-10-06T14:50:24.258Z" }, - { url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588, upload-time = "2025-10-06T14:50:25.716Z" }, - { url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966, upload-time = "2025-10-06T14:50:28.192Z" }, - { url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618, upload-time = "2025-10-06T14:50:29.82Z" }, - { url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539, upload-time = "2025-10-06T14:50:31.731Z" }, - { url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345, upload-time = "2025-10-06T14:50:33.26Z" }, - { url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934, upload-time = "2025-10-06T14:50:34.808Z" }, - { url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243, upload-time = "2025-10-06T14:50:36.436Z" }, - { url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878, upload-time = "2025-10-06T14:50:37.953Z" }, - { url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452, upload-time = "2025-10-06T14:50:39.574Z" }, - { url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312, upload-time = "2025-10-06T14:50:41.612Z" }, - { url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935, upload-time = "2025-10-06T14:50:43.972Z" }, - { url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385, upload-time = "2025-10-06T14:50:45.648Z" }, - { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777, upload-time = "2025-10-06T14:50:47.154Z" }, - { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104, upload-time = "2025-10-06T14:50:48.851Z" }, - { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503, upload-time = "2025-10-06T14:50:50.16Z" }, - { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, -] - [[package]] name = "mypy" version = "1.19.1" @@ -2036,15 +1476,6 @@ 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 = "narwhals" -version = "2.24.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2b/1d/58946e5aab18393e793bd4add6985b95d0e01c3a2d832f38f54468b10dcd/narwhals-2.24.0.tar.gz", hash = "sha256:b5c0f684ccd9d7475b564111e319a4964abcf2baf79d3cf6b1003d06ac9b828d", size = 661143, upload-time = "2026-07-13T10:49:19.086Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/85/a5bfaebfd305ac18b57b0854d74e37e586809061a91fda62f0bd50c8518e/narwhals-2.24.0-py3-none-any.whl", hash = "sha256:42fdedf44e5b2ca7505630d45b4ac3058f38d8485cba9fe1652ca23152df7489", size = 461030, upload-time = "2026-07-13T10:49:17.571Z" }, -] - [[package]] name = "nest-asyncio" version = "1.6.0" @@ -2054,22 +1485,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, ] -[[package]] -name = "nltk" -version = "3.10.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "defusedxml" }, - { name = "joblib" }, - { name = "regex" }, - { name = "tqdm" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/96/02/df4f105b28a7c16b0e41423bc09cf0f1b8a305df4ef0b10ca74a2e4c648c/nltk-3.10.0.tar.gz", hash = "sha256:4fbac1d98203cbcd1b5d94a2877fb822300072d80604a5e7fae49d2c5f84e8c1", size = 3089244, upload-time = "2026-07-08T02:39:13.562Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/89/a0b0f35e2820d6a99d75ea1c11977ee6d5c9e6658eceb45b0c7620881faa/nltk-3.10.0-py3-none-any.whl", hash = "sha256:54ff84d4916d3ef127e8953bee0023f6a6b320b75d634a19e06ef056d3d244bf", size = 1716144, upload-time = "2026-07-08T02:39:09.753Z" }, -] - [[package]] name = "numpy" version = "2.4.4" @@ -2422,44 +1837,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, ] -[[package]] -name = "pandas" -version = "3.0.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "python-dateutil" }, - { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712", size = 4658219, upload-time = "2026-07-22T22:19:28.819Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/ef/f1fd7431d635bf20015489bf0bd69c17fff1018de773540f651455a3916b/pandas-3.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2946e77e4a53cd248cbde631a12f0e51c8324ce354c3eba4d20147c1ad6f4282", size = 10397178, upload-time = "2026-07-22T22:17:48.274Z" }, - { url = "https://files.pythonhosted.org/packages/31/b4/0eafac990a431561187694126de01f9b12559549b4d86360c0c4bd870fde/pandas-3.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71ecc8fb7ed1a7aa4392316b5309a6347e8e7f832f38fd897846b3a1457a9298", size = 9990736, upload-time = "2026-07-22T22:17:52.388Z" }, - { url = "https://files.pythonhosted.org/packages/de/21/359880af3ea9b7cb23bea5b51e8e70ef3866c03be09da9a2787e18e330a8/pandas-3.0.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b173f5951ff6b8b0ec7675e20dff3c97b7e7a57dfcce387c2d7c5afe87cb7899", size = 10814438, upload-time = "2026-07-22T22:17:54.708Z" }, - { url = "https://files.pythonhosted.org/packages/d1/50/d6cc4d7e508bbccf5d6027314a8312bc7ac73d0ec7f195f53838daafab40/pandas-3.0.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c0cf1dd9b55a22d105fc46c1b489af3bd42264fcba7c66297bf47a9a1d9c78a", size = 11323634, upload-time = "2026-07-22T22:17:56.858Z" }, - { url = "https://files.pythonhosted.org/packages/70/2b/d5f0a8c90dd0ae04e64ba53b871afb796ec026b615086d382ddc2ade729b/pandas-3.0.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0fac0010c75e4efb6b99e249c183a8993ce0dc95c240f9b120a5e67c727b7928", size = 11850860, upload-time = "2026-07-22T22:17:59.1Z" }, - { url = "https://files.pythonhosted.org/packages/5c/30/183aec2e19adf778a98d29b5729a0a68f4cc4ebf9b9c3b70d0297355bcb1/pandas-3.0.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:08d24fe11a17dc33bd6e937dc9c665f9cba08fbdc9f657f405713515febe300d", size = 12411100, upload-time = "2026-07-22T22:18:01.485Z" }, - { url = "https://files.pythonhosted.org/packages/fa/9a/31f4983f191af51ab2a8f2d0c7b33dff3a84da26533f982fff02c2f9e28b/pandas-3.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:b1261758dfb6cf12c3cff8300e21cefad30e7ec709abb4c24ac7318e6a52462a", size = 9968804, upload-time = "2026-07-22T22:18:03.903Z" }, - { url = "https://files.pythonhosted.org/packages/49/97/7886c89a39045c69ad82cbceaf3343810480c8ef49a216319ce8183860a6/pandas-3.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:679f4e85b30ddb1515458ab1e788d3e260eae369b1f78da7a3aa4cac8ebf4a2a", size = 9205447, upload-time = "2026-07-22T22:18:06.134Z" }, - { url = "https://files.pythonhosted.org/packages/1c/54/1dc810ea558d1320b597aa140a514f2fdf1d2ea09c38cf556f13ea712ec9/pandas-3.0.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa290c16964d4963fbfbc358928239cf3bd755b20e988ce944877def2f44471d", size = 10411717, upload-time = "2026-07-22T22:18:08.307Z" }, - { url = "https://files.pythonhosted.org/packages/68/56/fbe81c09195924d8b7b8d4461a20458fe80a6a5ed6b24f0314da684277e1/pandas-3.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2e26bb46934b8a2ca0c3de1d3d606fc5f6746584791b2db264d58cf370e08dc", size = 9957095, upload-time = "2026-07-22T22:18:10.6Z" }, - { url = "https://files.pythonhosted.org/packages/e0/51/fac252f4a913ed5eabf3c11b880a9e8d5a6c10f0b2129d0462212d238b4d/pandas-3.0.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73fa87b08a7ef706f8aafda39ddaccf2a99047bea62d8c88a0361bcafb2237bc", size = 10485458, upload-time = "2026-07-22T22:18:12.834Z" }, - { url = "https://files.pythonhosted.org/packages/12/98/e976540c1addf70442be7842a18cf70884a964abbf69442504f4d2939989/pandas-3.0.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d373ce03ffd84010ed9839fa73672a9c8256990532e158440c0085db7d914b34", size = 10998091, upload-time = "2026-07-22T22:18:15.209Z" }, - { url = "https://files.pythonhosted.org/packages/a4/8c/1f29b5be8d3fc47dd7567eb167fabba2085879b31e0287ce7cba6d3d2ff4/pandas-3.0.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a29c53d85ea98c5e792c59ef82ee9fbe6ca902c0d0adb6b23f45ef894cd7bf6", size = 11499501, upload-time = "2026-07-22T22:18:17.689Z" }, - { url = "https://files.pythonhosted.org/packages/9d/e2/bd9c98ad2df7b38bde002adde4cdf353519da51881634323b126c55997f9/pandas-3.0.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a5ad3b02ed6bc7d7ae9b70804b2c6aa31827489d150f8e623ce82491b82085d7", size = 12060559, upload-time = "2026-07-22T22:18:20.147Z" }, - { url = "https://files.pythonhosted.org/packages/f3/9a/ffbd852d58bd74a617fe2f8ee6a58a96982271ce41cf981eab22190b4a4b/pandas-3.0.5-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:b2acb4650527eec6822c3dadb2b771277b65e7dae7a267d4bccf65fd1bb3fbce", size = 7197652, upload-time = "2026-07-22T22:18:22.502Z" }, - { url = "https://files.pythonhosted.org/packages/70/b5/d2d3e9ae73362ba4229651b0ee1455cf78073a1ce585f6ff693782ce263e/pandas-3.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:80a611068e8a3ac23f7398c6c14eb46dc974e5cc9997f653e2dcfd1da74edd41", size = 9831691, upload-time = "2026-07-22T22:18:24.534Z" }, - { url = "https://files.pythonhosted.org/packages/52/51/dea1e89d6a6796b9c43f85a09b484ee03edb8a4c4842e73e200a8c11301c/pandas-3.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:25ff585b972a18ef1fe9ffa3ac6544d9950508aa76832e5147640b6022821e49", size = 9105796, upload-time = "2026-07-22T22:18:27.064Z" }, - { url = "https://files.pythonhosted.org/packages/bf/09/7b95c4a0025227d6f118c4039b423412ac6a982db02864166185d812fbc7/pandas-3.0.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c1c05a767fe8e5b4fe9e1c29806829c582052eaedb9120a3da83ba3f69e24a5b", size = 10385742, upload-time = "2026-07-22T22:18:29.346Z" }, - { url = "https://files.pythonhosted.org/packages/8d/0c/dc78fd8c4da477b4b5e8ad37295af352190d21ef63a9ee1bc071753074cc/pandas-3.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b86765f268b56f7e665b93bce9d5df69dee7f99e595cf8fb839483ab315942a3", size = 9932067, upload-time = "2026-07-22T22:18:31.833Z" }, - { url = "https://files.pythonhosted.org/packages/3e/71/3592c055cf44df9808550f9368ceda80ff2b224d355ef73fe251dcda1802/pandas-3.0.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c597ecf5616b5c420372c1d4d4c00dbbfba7398bea857dcc984347e1ea48417b", size = 10466756, upload-time = "2026-07-22T22:18:34.195Z" }, - { url = "https://files.pythonhosted.org/packages/e3/70/4363150359f95b4cb4bcbb34ca23572bb5495749a621a8f3d5a1ddfd293c/pandas-3.0.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b11c36e218331d0387cbe3a0a5f75162357a1d92d57b2b08a336ff94b19b2be", size = 10938525, upload-time = "2026-07-22T22:18:36.81Z" }, - { url = "https://files.pythonhosted.org/packages/f7/d0/317e7a0c67c0e69fa905a0161409397a7dc2d46ff611f6ca4803352c042b/pandas-3.0.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf52e1f61d229496da17dc7ab54acdee627357e7008fd4fecba3d0ba2937fa58", size = 11489303, upload-time = "2026-07-22T22:18:39.287Z" }, - { url = "https://files.pythonhosted.org/packages/f1/8d/36dade89b49e4f9d5cbdbe863772581f98c0c6d78fc39ad4c557f6f2e17e/pandas-3.0.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:db172144bb56422bd157812f3b021eacc255451470b31e2c633c349490a1cfee", size = 11989004, upload-time = "2026-07-22T22:18:42.208Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ba/18c4ec8a746e177da05a9e7a7963781d8ea195780724f854601b6ebd6b78/pandas-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:0d298e951f23016ce4699951d044ae6418dbc91bf68cefca0f77666fcbb4e5c6", size = 9826896, upload-time = "2026-07-22T22:18:44.539Z" }, - { url = "https://files.pythonhosted.org/packages/de/ec/28a57266b753799a87b8bc79e7887ac6fd981b8c6d2978a0b7e7b6bd708c/pandas-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:66266d3442a5e8b3c90274c2b8b230bee42dd1c286bc822cc2f9f2c7e12b883e", size = 9094790, upload-time = "2026-07-22T22:18:47.468Z" }, -] - [[package]] name = "parso" version = "0.8.5" @@ -2547,75 +1924,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, ] -[[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/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 = "proto-plus" version = "1.27.0" @@ -3160,18 +2468,6 @@ 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 = "rouge-score" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "absl-py" }, - { name = "nltk" }, - { name = "numpy" }, - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e2/c5/9136736c37022a6ad27fea38f3111eb8f02fe75d067f9a985cc358653102/rouge_score-0.1.2.tar.gz", hash = "sha256:c7d4da2683e68c9abf0135ef915d63a46643666f848e558a1b9f7ead17ff0f04", size = 17400, upload-time = "2022-07-22T22:46:22.909Z" } - [[package]] name = "rpds-py" version = "0.30.0" @@ -3251,15 +2547,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, ] -[[package]] -name = "ruamel-yaml" -version = "0.19.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, -] - [[package]] name = "ruff" version = "0.14.14" @@ -3298,144 +2585,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bc/e7/5c595c75e9f41a44f30e526eda465ea0b4eec93470e074e4a111b253f13a/s3transfer-0.19.2-py3-none-any.whl", hash = "sha256:d8168eccca828cbb2cd573675333f3bddd254313a9c42494b84c76b539e8ba25", size = 90216, upload-time = "2026-07-22T19:30:43.251Z" }, ] -[[package]] -name = "scikit-learn" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "joblib" }, - { name = "narwhals" }, - { name = "numpy" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, - { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "threadpoolctl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fa/6f/37092bdb25f712817231799fc5674d8e704066a8a70c1d2d40517e18b4ab/scikit_learn-1.9.0.tar.gz", hash = "sha256:8833266989d3a5110178a9fae30783675460724d0e1efb13b14901d2c660c557", size = 7750767, upload-time = "2026-06-02T11:54:32.706Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/be/e844fd9586e66540a15b71924d17a6cbc1bb749e81ddd0a796bcdba4c055/scikit_learn-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9db6f4d34e68c8899e4cab27fdf8eafe6ed21f2ba52ceb25ea250cd237f8e47b", size = 8789686, upload-time = "2026-06-02T11:53:05.439Z" }, - { url = "https://files.pythonhosted.org/packages/42/e2/ff880f62677a17d035817d543cb0fc8727d01eccbee81c5f7fc733a9d856/scikit_learn-1.9.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f401448645a3e7bc115aa3c094097865155b34bff1cba8101857d9104e99074c", size = 8256782, upload-time = "2026-06-02T11:53:08.904Z" }, - { url = "https://files.pythonhosted.org/packages/25/64/eb40435e1a508ab1b4e284ce43ae80f6a162e5be5e38ed5a6fab467a9ea4/scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd3a8ef0c758555a3b23c03adaa858af32f7736785ded50ad5991f59c4ed03fa", size = 8992419, upload-time = "2026-06-02T11:53:11.551Z" }, - { url = "https://files.pythonhosted.org/packages/8d/da/4810a28e473185429e45a57eebcc91fc991b33d889cc0676063e671db03d/scikit_learn-1.9.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7e254636164090da847715a27f8e5478feb98c40a9e0ee90cbd277de9e5ceb8", size = 9281411, upload-time = "2026-06-02T11:53:15.063Z" }, - { url = "https://files.pythonhosted.org/packages/3b/67/be3d369f40d8178ba3bd86635d132e08cb5329b023e4669d9426d84bc007/scikit_learn-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:5dc1818c77575d149e25fce9ef82dd7b7263ae372f03494158668ad632a69759", size = 8272736, upload-time = "2026-06-02T11:53:18.108Z" }, - { url = "https://files.pythonhosted.org/packages/37/79/a733f02dc2118da7e77a134b34f39f40201a353311b011d20859d2db3556/scikit_learn-1.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:366652351f092b219c248f1e72821e841960a63d8f358f1dcfd54dc1cbdbbc28", size = 7919564, upload-time = "2026-06-02T11:53:21.2Z" }, - { url = "https://files.pythonhosted.org/packages/ac/20/75f915ff375d6249e6550ac740fdbbd66159a068fd3af1400ff62036b07a/scikit_learn-1.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2bd41b0d201bc81575531b96b713d3eb5e5f50fb0b82101ff0f92294fdc236ac", size = 8741122, upload-time = "2026-06-02T11:53:24.08Z" }, - { url = "https://files.pythonhosted.org/packages/cc/d5/2b5148f2279196775e1db2aeb85d14b70ac80e7e32b3b28e7ebeafb0901d/scikit_learn-1.9.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5be45aa4a42a68a533913a6ed736cf309de2226411c79ef8d609a5456f1939b1", size = 8261512, upload-time = "2026-06-02T11:53:27.183Z" }, - { url = "https://files.pythonhosted.org/packages/a0/ee/5adbc77656b71f9456a2f5a7a9fdb4bcf9207a6b962889f1c2f9323afa4e/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e50ed4da51974e86e940690e9a3d82e729b62b5a49f7c9bac534d515d39d86f", size = 8837603, upload-time = "2026-06-02T11:53:30.328Z" }, - { url = "https://files.pythonhosted.org/packages/6c/c2/63fdda36c56437eeb44aaf9493c8bcd62ce230ab1598924fc626ffbfa943/scikit_learn-1.9.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:056c92bb67ad4c28463c2f2653d9701449201e7e7a9e94e321be0f71c4fef2b8", size = 9132097, upload-time = "2026-06-02T11:53:33.456Z" }, - { url = "https://files.pythonhosted.org/packages/83/a4/c8e67227c680e2259c8864ae72ff48b06e16a6f51253a22167aa02a8aa4e/scikit_learn-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:4306775fad04cc4b472a1b15af1ae9cede1540fbfcc17fbce3767cd8dc7ae283", size = 8211173, upload-time = "2026-06-02T11:53:36.602Z" }, - { url = "https://files.pythonhosted.org/packages/cf/fd/3c0863792e98e67e9184aa4029288a175935eb65443afcd30d4f143450cf/scikit_learn-1.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:26e22435f63bcdcf396b574273f29f13dd531f5ea035801f5be10ba1540a4e60", size = 7867451, upload-time = "2026-06-02T11:53:39.075Z" }, - { url = "https://files.pythonhosted.org/packages/3c/01/cf3310626b6d48d3e9be69a1223f9180360b5e6edb045f50fade723ce494/scikit_learn-1.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:80746d63bd4b6eaca54d36fe5feaf4d28bb38dc6f9470f81c7cad7c40155f119", size = 8705188, upload-time = "2026-06-02T11:53:41.964Z" }, - { url = "https://files.pythonhosted.org/packages/3e/04/5acd7ae280c5f93b6ac5ef6cdec14eef4c8d1cd91d85b3292989c94d96b1/scikit_learn-1.9.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5b934c45c252844a91d69fda3a34cff5e7307e1db10d77cb10a3980312c74713", size = 8228299, upload-time = "2026-06-02T11:53:44.817Z" }, - { url = "https://files.pythonhosted.org/packages/0c/39/ffe829a5b8ecb40a518724a997794657fdc354ada5e8fe8e64d998c0bac9/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:38c3dcb9a1ffb85505ec53d54c7b4aea0cff70050425a7760c2af661ac85df05", size = 8789690, upload-time = "2026-06-02T11:53:47.461Z" }, - { url = "https://files.pythonhosted.org/packages/1f/88/8dab5de10c638c083772a6be83a3d8106ced492f74a928c8693638e5bb50/scikit_learn-1.9.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da76d09304a4706db7cc1e3ebaa3b6b98a67365cc11d2996c4f1e58ba47df714", size = 9087723, upload-time = "2026-06-02T11:53:50.702Z" }, - { url = "https://files.pythonhosted.org/packages/20/3f/7917ca72464038f6240ec70c29f94862d08a34a74291ae4d4ec5eb8186a0/scikit_learn-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5808d98f15c6bf6d9d96d2348c1997392a5888ce7097e664105f930c4bca1277", size = 8184330, upload-time = "2026-06-02T11:53:53.396Z" }, - { url = "https://files.pythonhosted.org/packages/78/c7/15739eb2f61fda3c54639e9942414e5a19ad8a8d1f5a3266afad7cb7df80/scikit_learn-1.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:d77f54c017633791bc0225a43e2f8d03745fdcfe4880268fcc4df15f505dec2e", size = 7840653, upload-time = "2026-06-02T11:53:56.035Z" }, -] - -[[package]] -name = "scipy" -version = "1.17.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.12' and sys_platform == 'win32'", - "python_full_version < '3.12' and sys_platform == 'emscripten'", - "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] -dependencies = [ - { name = "numpy", marker = "python_full_version < '3.12'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, - { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, - { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, - { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, - { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, - { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, - { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, - { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, - { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, - { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, - { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, - { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, - { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, - { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, - { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, - { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, - { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, - { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, - { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, - { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, - { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, - { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, - { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, - { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, - { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, - { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, - { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, - { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, - { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, - { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, - { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, - { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, -] - -[[package]] -name = "scipy" -version = "1.18.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.13' and sys_platform == 'win32'", - "python_full_version >= '3.13' and sys_platform == 'emscripten'", - "python_full_version >= '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'win32'", - "python_full_version == '3.12.*' and sys_platform == 'emscripten'", - "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", -] -dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.12'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/19/ca10ead60b0acc80b2b833c2c4a4f2ff753d0f58b811f70d911c7e94a25c/scipy-1.18.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a", size = 31056519, upload-time = "2026-06-19T14:59:45.203Z" }, - { url = "https://files.pythonhosted.org/packages/96/72/1e6442a00cd2924d361aa1b642ab6373ec35c6fabf311a760be9f76e0f13/scipy-1.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b", size = 28681889, upload-time = "2026-06-19T14:59:48.103Z" }, - { url = "https://files.pythonhosted.org/packages/9b/2d/11dd93d21e147a73ba22bd75c0b9208d3a2e0ec76d53170ce7d9029b1015/scipy-1.18.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9", size = 20423580, upload-time = "2026-06-19T14:59:50.665Z" }, - { url = "https://files.pythonhosted.org/packages/9c/01/93552f75e0d2a7dd115a45e59209c51e8d514daff02fc887d2623be06fe1/scipy-1.18.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8", size = 23054441, upload-time = "2026-06-19T14:59:53.564Z" }, - { url = "https://files.pythonhosted.org/packages/3c/23/21f5e703643d66f21faa6b4c73195bfcad70c55efcb4f1ab327cd7c4101a/scipy-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab", size = 33968720, upload-time = "2026-06-19T14:59:56.415Z" }, - { url = "https://files.pythonhosted.org/packages/dd/aa/1b939f6c67ed68635bb538e6752d3dacc02f66535182e939a89581a44e9c/scipy-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2", size = 35287115, upload-time = "2026-06-19T14:59:59.411Z" }, - { url = "https://files.pythonhosted.org/packages/b6/ff/eec46be7e9234208f801062b53e1983085eddebd693f6c9bfb03b459830d/scipy-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11", size = 35577989, upload-time = "2026-06-19T15:00:02.235Z" }, - { url = "https://files.pythonhosted.org/packages/84/ca/210d4759c7210bb7d269437421959b39a33434e2776b60c5cb8a763bb30a/scipy-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de", size = 37421717, upload-time = "2026-06-19T15:00:05.102Z" }, - { url = "https://files.pythonhosted.org/packages/2b/54/9a9edb45345bd6744da5ddfb6628e5d5185920494c6a67ec45b6381004cb/scipy-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132", size = 36597428, upload-time = "2026-06-19T15:00:08.112Z" }, - { url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481, upload-time = "2026-06-19T15:00:11.103Z" }, - { url = "https://files.pythonhosted.org/packages/05/52/9c0136c2de7ae0779b7b366447766cec6d9f0702c56bb8ffeb04c8fd3af4/scipy-1.18.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:09143f676d157d9f546d663504ef9c1becb819824f1afc018814176411942446", size = 31036107, upload-time = "2026-06-19T15:00:14.03Z" }, - { url = "https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5efe260f69417b97ddae455bfb5a95e8359f7f66ad7fa9522a60feb66f169520", size = 28663303, upload-time = "2026-06-19T15:00:16.819Z" }, - { url = "https://files.pythonhosted.org/packages/d3/0f/10ffa0b697a572f4e0d48b92a88895d366422f019f723e7e14a84c050dac/scipy-1.18.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:68363b7eaacd8b5dd426df56d782cc156468ac79a127a1b87ca597d6e2e82197", size = 20404960, upload-time = "2026-06-19T15:00:19.635Z" }, - { url = "https://files.pythonhosted.org/packages/7e/d2/e896cea21ba8edd6c81d4c55b1ffcc717e79698dcbebf9641b4cfb4c6622/scipy-1.18.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:c5557d8be5da8e41353fcd4d21491fdbab83b062fc579e94dc09a7c8ab4f669b", size = 23034074, upload-time = "2026-06-19T15:00:22.107Z" }, - { url = "https://files.pythonhosted.org/packages/ea/b2/e83ea34279a52c03374477c74006256ec78df65fc877baa4617d6de1d202/scipy-1.18.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d13bca67c096d89fb95ced0d8921807300fce0275643aef9533cc63a0773468", size = 33942038, upload-time = "2026-06-19T15:00:24.964Z" }, - { url = "https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a46f9273dbd0eb1cefba61c9b8648b4dfe3cbc14a080176f9a73e44b8336dc7f", size = 35266390, upload-time = "2026-06-19T15:00:28.059Z" }, - { url = "https://files.pythonhosted.org/packages/3a/49/2c5cbb907b56695fc67517811d1db234dfd83381a84814ec220aded2794d/scipy-1.18.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5aba46108853ddfc77906b6557aac839d2b52e900c1d72a1180adaaab58d265f", size = 35551324, upload-time = "2026-06-19T15:00:31.014Z" }, - { url = "https://files.pythonhosted.org/packages/bb/73/eda39f7a2d306ff0ffc574afd13c0bbb6d10a603d9a413998ee269487a80/scipy-1.18.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6f758e35f12757b5d95c00bc6de2438e229c2664b7a92e96f205959d9f2dfa4", size = 37404785, upload-time = "2026-06-19T15:00:34.072Z" }, - { url = "https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:1afac4a847207c7ff8efd321734a50b06d0280b3b2a2c0fc2f413101747ad7c7", size = 36554943, upload-time = "2026-06-19T15:00:36.903Z" }, - { url = "https://files.pythonhosted.org/packages/70/3a/21154e2d54eb3639c6bf4dbae2e531c68356bfe95990daa30df33b30d556/scipy-1.18.0-cp313-cp313-win_arm64.whl", hash = "sha256:c5dbddf60e58c2312316d097271a8e73d40eaf2eabfa4d95ed7d3695bbf2ce7b", size = 24350911, upload-time = "2026-06-19T15:00:40.062Z" }, -] - -[[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" @@ -3532,15 +2681,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, ] -[[package]] -name = "tabulate" -version = "0.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, -] - [[package]] name = "tenacity" version = "9.1.2" @@ -3550,81 +2690,6 @@ 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 = "threadpoolctl" -version = "3.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, -] - -[[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" }, -] - -[[package]] -name = "tokenizers" -version = "0.22.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "huggingface-hub" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, - { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, - { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, - { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, - { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, - { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, - { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, - { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, - { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, - { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, - { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, - { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, - { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, - { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, - { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, -] - [[package]] name = "tomli" version = "2.4.1" @@ -3701,19 +2766,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, ] -[[package]] -name = "typer-slim" -version = "0.21.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/17/d4/064570dec6358aa9049d4708e4a10407d74c99258f8b2136bb8702303f1a/typer_slim-0.21.1.tar.gz", hash = "sha256:73495dd08c2d0940d611c5a8c04e91c2a0a98600cbd4ee19192255a233b6dbfd", size = 110478, upload-time = "2026-01-06T11:21:11.176Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/0a/4aca634faf693e33004796b6cee0ae2e1dba375a800c16ab8d3eff4bb800/typer_slim-0.21.1-py3-none-any.whl", hash = "sha256:6e6c31047f171ac93cc5a973c9e617dbc5ab2bddc4d0a3135dc161b4e2020e0d", size = 47444, upload-time = "2026-01-06T11:21:12.441Z" }, -] - [[package]] name = "types-grpcio" version = "1.0.0.20251009" @@ -3912,90 +2964,3 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" }, { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" }, ] - -[[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/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 = "zipp" -version = "3.23.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, -]