diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..da1061a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,49 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + lint: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: pip install ruff + - name: Ruff check + run: ruff check agent_memory_toolkit/ tests/ + - name: Ruff format check + run: ruff format --check agent_memory_toolkit/ tests/ + + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install package with dev dependencies + run: pip install -e ".[dev]" + - name: Run unit tests with coverage + run: pytest tests/unit/ --cov=agent_memory_toolkit --cov-report=xml --cov-report=term-missing -v + - name: Upload coverage + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-report-${{ matrix.python-version }} + path: coverage.xml diff --git a/Docs/azure_testing.md b/Docs/azure_testing.md index b6aaf57..9523358 100644 --- a/Docs/azure_testing.md +++ b/Docs/azure_testing.md @@ -22,12 +22,12 @@ You need: - an Azure subscription - `az login` -- Python 3.10+ +- Python 3.11+ - Azure Functions Core Tools v4 - dependencies installed: ```bash -pip install -r requirements.txt +pip install -e ".[dev]" pip install -r azure_functions/requirements.txt ``` @@ -190,7 +190,7 @@ memory.connect_cosmos() import os from dotenv import load_dotenv from azure.identity.aio import DefaultAzureCredential as AsyncDefaultAzureCredential -from agent_memory_toolkit import AsyncAgentMemory +from agent_memory_toolkit.aio import AsyncAgentMemory load_dotenv() diff --git a/Docs/design_patterns.md b/Docs/design_patterns.md index 700b1df..c55105b 100644 --- a/Docs/design_patterns.md +++ b/Docs/design_patterns.md @@ -12,7 +12,7 @@ Write a turn memory every time a user or agent message is produced. If the appli ```python from azure.identity.aio import DefaultAzureCredential as AsyncDefaultAzureCredential -from agent_memory_toolkit import AsyncAgentMemory +from agent_memory_toolkit.aio import AsyncAgentMemory mem = AsyncAgentMemory( cosmos_endpoint=COSMOS_ENDPOINT, diff --git a/Docs/local_testing.md b/Docs/local_testing.md index a181732..c589729 100644 --- a/Docs/local_testing.md +++ b/Docs/local_testing.md @@ -10,7 +10,7 @@ This guide covers the shortest path to running the library locally, then validat | Tool | Install command | Purpose | |------|-----------------|---------| -| Python 3.10+ | `brew install python@3.12` | Runtime | +| Python 3.11+ | `brew install python@3.13` | Runtime | | Azure CLI | `brew install azure-cli` | `az login` for `DefaultAzureCredential` | | Azure Functions Core Tools v4 | `brew install azure-functions-core-tools@4` | Run Functions locally | | Azurite | `npm install -g azurite` | Local storage emulator for Functions | @@ -19,7 +19,7 @@ This guide covers the shortest path to running the library locally, then validat ### Python packages ```bash -pip install -r requirements.txt +pip install -e ".[dev]" pip install -r azure_functions/requirements.txt ``` @@ -168,7 +168,7 @@ for r in results: import os, uuid from dotenv import load_dotenv from azure.identity.aio import DefaultAzureCredential as AsyncDefaultAzureCredential -from agent_memory_toolkit import AsyncAgentMemory +from agent_memory_toolkit.aio import AsyncAgentMemory load_dotenv() @@ -217,7 +217,7 @@ azurite --silent --location /tmp/azurite --debug /tmp/azurite/debug.log ```bash cd azure_functions -pip install -r requirements.txt +pip install -r azure_functions/requirements.txt func start ``` @@ -324,7 +324,7 @@ User summaries also update incrementally when one already exists. | Problem | Fix | |---------|-----| -| `ImportError: azure.identity` | Run `pip install -r requirements.txt` | +| `ImportError: azure.identity` | Run `pip install -e ".[dev]"` | | `DefaultAzureCredential` fails | Run `az login` and confirm the active subscription | | Cosmos 403 | Check Cosmos DB RBAC and wait for propagation | | `func: command not found` | Install Azure Functions Core Tools v4 | @@ -334,6 +334,3 @@ User summaries also update incrementally when one already exists. | Function 401 in Azure | Set `ADF_KEY` or pass `?code=` | For full cloud deployment and validation, see `Docs/azure_testing.md`. -| Functions host can't connect to storage | Make sure Azurite is running before starting `func start` | -| Embeddings 401/403 | Confirm the "Cognitive Services OpenAI User" role is assigned to your identity on the AI resource | -| Functions 401 Unauthorized | The HTTP trigger requires a function key. Pass it as `?code=` in the URL or set `ADF_KEY` in `.env`. See "Get the function key" above. | diff --git a/README.md b/README.md index 7dd5a1d..7b35cef 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Azure Cosmos DB Agent Memory Toolkit [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) -[![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/) +[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) [![Azure Cosmos DB](https://img.shields.io/badge/Azure-Cosmos%20DB-0078D4?logo=microsoft-azure)](https://azure.microsoft.com/en-us/products/cosmos-db/) [![Follow on X](https://img.shields.io/twitter/follow/AzureCosmosDB?style=social)](https://twitter.com/AzureCosmosDB) [![LinkedIn](https://img.shields.io/badge/LinkedIn-Azure%20Cosmos%20DB-0077B5?logo=linkedin)](https://www.linkedin.com/showcase/azure-cosmos-db/) @@ -71,11 +71,24 @@ Agent Memory Toolkit is a Python library and Azure-backed reference implementati ## Project Structure ``` -agent_memory_toolkit/ Python library — AgentMemory (sync) + AsyncAgentMemory (async) +agent_memory_toolkit/ Python library — sync API + memory.py AgentMemory orchestrator + cosmos_memory_client.py CosmosMemoryStore — Cosmos DB CRUD + vector search + embeddings.py EmbeddingsClient — Azure OpenAI embeddings + processing.py ProcessingClient — Durable Functions polling + models.py Pydantic data models (MemoryRecord, enums) + exceptions.py Custom exception hierarchy + _query_builder.py Shared query builder (private) + aio/ Async API (mirrors azure.cosmos.aio convention) + memory.py AsyncAgentMemory + cosmos_memory_client.py AsyncCosmosMemoryStore + embeddings.py AsyncEmbeddingsClient + processing.py AsyncProcessingClient azure_functions/ Durable Functions — orchestrator, activities, HTTP trigger prompts/ LLM system prompts — summarize, facts, user_summary + update variants Samples/ Demo notebooks — sync (Demo.ipynb) + async (Demo_async.ipynb) Docs/ Documentation — concepts, local testing, Azure deployment +tests/ Unit tests (pytest) — 184 tests, 87% coverage ``` --- @@ -85,7 +98,10 @@ Docs/ Documentation — concepts, local testing, Azure ### 1. Install ```bash -pip install -r requirements.txt +pip install . + +# With dev/test dependencies +pip install ".[dev]" ``` ### 2. Local-only (no Azure) @@ -156,7 +172,11 @@ result = memory.generate_user_summary(user_id="user-001") summary = memory.get_user_summary(user_id="user-001") ``` -> The async API (`AsyncAgentMemory`) is identical — just `await` each call. +> The async API (`AsyncAgentMemory`) is identical — just `await` each call. Import from the `aio` subpackage: +> +> ```python +> from agent_memory_toolkit.aio import AsyncAgentMemory +> ``` --- diff --git a/Samples/Demo_async.ipynb b/Samples/Demo_async.ipynb index 04baa73..8669aa7 100644 --- a/Samples/Demo_async.ipynb +++ b/Samples/Demo_async.ipynb @@ -43,7 +43,7 @@ "sys.path.insert(0, os.path.abspath(\"..\"))\n", "\n", "from dotenv import load_dotenv\n", - "from agent_memory_toolkit import AsyncAgentMemory\n", + "from agent_memory_toolkit.aio import AsyncAgentMemory\n", "\n", "# Load environment variables from .env in the repo root\n", "load_dotenv(os.path.join(\"..\", \".env\"))\n", diff --git a/agent_memory_toolkit/__init__.py b/agent_memory_toolkit/__init__.py index 50ecbc1..aff6904 100644 --- a/agent_memory_toolkit/__init__.py +++ b/agent_memory_toolkit/__init__.py @@ -1,6 +1,25 @@ """Agent Memory Toolkit – local and cloud agent memory management.""" +from agent_memory_toolkit.aio import AsyncAgentMemory +from agent_memory_toolkit.exceptions import ( + AgentMemoryError, + AuthenticationError, + ConfigurationError, + CosmosNotConnectedError, + CosmosOperationError, + EmbeddingError, + MemoryNotFoundError, + OrchestrationTimeoutError, + ProcessingError, + ValidationError, +) from agent_memory_toolkit.memory import AgentMemory -from agent_memory_toolkit.async_memory import AsyncAgentMemory +from agent_memory_toolkit.models import MemoryRecord, MemoryRole, MemoryType, SearchResult -__all__ = ["AgentMemory", "AsyncAgentMemory"] +__all__ = [ + "AgentMemory", "AsyncAgentMemory", + "MemoryRecord", "MemoryRole", "MemoryType", "SearchResult", + "AgentMemoryError", "ConfigurationError", "ValidationError", + "CosmosNotConnectedError", "CosmosOperationError", "MemoryNotFoundError", + "EmbeddingError", "ProcessingError", "OrchestrationTimeoutError", "AuthenticationError", +] diff --git a/agent_memory_toolkit/_query_builder.py b/agent_memory_toolkit/_query_builder.py new file mode 100644 index 0000000..7af0109 --- /dev/null +++ b/agent_memory_toolkit/_query_builder.py @@ -0,0 +1,43 @@ +"""Reusable query-builder for parameterized Cosmos DB queries. + +The :class:`_QueryBuilder` helper eliminates duplicated +condition/parameter-building patterns across the sync and async clients. +""" + +from __future__ import annotations + +from typing import Any + + +class _QueryBuilder: + """Accumulates optional WHERE conditions and their parameterized values. + + Usage:: + + qb = _QueryBuilder() + qb.add_filter("c.user_id", "@user_id", some_user_id) + qb.add_filter("c.role", "@role", some_role) + where = qb.build_where() # " WHERE c.user_id = @user_id AND c.role = @role" + params = qb.get_parameters() # [{"name": "@user_id", "value": ...}, ...] + """ + + def __init__(self) -> None: + self._conditions: list[str] = [] + self._parameters: list[dict[str, Any]] = [] + + def add_filter(self, field: str, param_name: str, value: Any) -> None: + """Add a filter only when *value* is not ``None``.""" + if value is None: + return + self._conditions.append(f"{field} = {param_name}") + self._parameters.append({"name": param_name, "value": value}) + + def build_where(self) -> str: + """Return the ``WHERE …`` clause (or empty string if no filters).""" + if not self._conditions: + return "" + return " WHERE " + " AND ".join(self._conditions) + + def get_parameters(self) -> list[dict[str, Any]]: + """Return a *copy* of the accumulated parameters list.""" + return list(self._parameters) diff --git a/agent_memory_toolkit/_utils.py b/agent_memory_toolkit/_utils.py new file mode 100644 index 0000000..936146f --- /dev/null +++ b/agent_memory_toolkit/_utils.py @@ -0,0 +1,154 @@ +"""Shared utilities for the Agent Memory Toolkit. + +Houses helpers used by both the sync and async clients to avoid +duplication and hidden cross-module coupling. +""" + +from __future__ import annotations + +import os +import uuid +from datetime import datetime, timezone +from typing import Any, Optional + +from ._query_builder import _QueryBuilder +from .exceptions import ConfigurationError, MemoryNotFoundError, ValidationError + +# --------------------------------------------------------------------------- +# Validation constants +# --------------------------------------------------------------------------- + +VALID_ROLES = {"agent", "user", "tool", "system"} +VALID_TYPES = {"turn", "summary", "fact", "user_summary"} + + +# --------------------------------------------------------------------------- +# Memory factory +# --------------------------------------------------------------------------- + + +def _make_memory( + user_id: str, + role: str, + content: str, + memory_type: str = "turn", + agent_id: Optional[str] = None, + metadata: Optional[dict[str, Any]] = None, + memory_id: Optional[str] = None, + thread_id: Optional[str] = None, +) -> dict[str, Any]: + """Create a validated memory dict.""" + if role not in VALID_ROLES: + raise ValidationError(f"role must be one of {VALID_ROLES}, got '{role}'") + if memory_type not in VALID_TYPES: + raise ValidationError(f"type must be one of {VALID_TYPES}, got '{memory_type}'") + + memory: dict[str, Any] = { + "id": memory_id or str(uuid.uuid4()), + "user_id": user_id, + "thread_id": thread_id or str(uuid.uuid4()), + "role": role, + "type": memory_type, + "content": content, + "metadata": metadata or {}, + "created_at": datetime.now(timezone.utc).isoformat(), + } + + if agent_id is not None: + memory["agent_id"] = agent_id + + return memory + + +def _resolve_embedding_dimensions(val: Optional[int]) -> Optional[int]: + """Resolve embedding dimensions from explicit value or ``EMBEDDING_DIMENSIONS`` env var.""" + if val is not None: + return val + raw = os.environ.get("EMBEDDING_DIMENSIONS", "0") or "0" + parsed = int(raw) + return parsed if parsed else None + + +# --------------------------------------------------------------------------- +# Connection / query helpers (shared by sync & async Cosmos clients) +# --------------------------------------------------------------------------- + + +def _validate_connection( + endpoint: str | None, + credential: Any, + database: str, + container: str, +) -> None: + """Raise :class:`ConfigurationError` if any required field is missing.""" + if not endpoint: + raise ConfigurationError(parameter="endpoint") + if not credential: + raise ConfigurationError(parameter="credential") + if not database: + raise ConfigurationError(parameter="database") + if not container: + raise ConfigurationError(parameter="container") + + +def _build_memory_query_builder( + *, + memory_id: Optional[str] = None, + user_id: Optional[str] = None, + thread_id: Optional[str] = None, + role: Optional[str] = None, + memory_type: Optional[str] = None, +) -> _QueryBuilder: + """Return a :class:`_QueryBuilder` pre-loaded with the standard filters.""" + qb = _QueryBuilder() + qb.add_filter("c.id", "@memory_id", memory_id) + qb.add_filter("c.user_id", "@user_id", user_id) + qb.add_filter("c.thread_id", "@thread_id", thread_id) + qb.add_filter("c.role", "@role", role) + qb.add_filter("c.type", "@memory_type", memory_type) + return qb + + +def _container_policies( + *, + embedding_dimensions: int, + embedding_data_type: str, + distance_function: str, + full_text_language: str, +) -> tuple[dict, dict, dict]: + """Build the vector, indexing, and full-text policies for container creation.""" + vector_embedding_policy = { + "vectorEmbeddings": [ + { + "path": "/embedding", + "dataType": embedding_data_type, + "distanceFunction": distance_function, + "dimensions": embedding_dimensions, + } + ] + } + + indexing_policy = { + "includedPaths": [{"path": "/*"}], + "excludedPaths": [{"path": "/embedding/*"}], + "vectorIndexes": [{"path": "/embedding", "type": "quantizedFlat"}], + "fullTextIndexes": [{"path": "/content"}], + } + + full_text_policy = { + "defaultLanguage": full_text_language, + "fullTextPaths": [{"path": "/content", "language": full_text_language}], + } + + return vector_embedding_policy, indexing_policy, full_text_policy + + +def _validate_hybrid_search( + hybrid_search: bool, + search_terms: Optional[str], +) -> None: + """Raise :class:`ValidationError` if hybrid search is requested without search terms.""" + if hybrid_search and not search_terms: + raise ValidationError( + "search_terms is required when hybrid_search is True" + ) diff --git a/agent_memory_toolkit/aio/__init__.py b/agent_memory_toolkit/aio/__init__.py new file mode 100644 index 0000000..c9e1d03 --- /dev/null +++ b/agent_memory_toolkit/aio/__init__.py @@ -0,0 +1,17 @@ +"""Async variants of the Agent Memory Toolkit clients. + +This subpackage mirrors the sync API surface at ``agent_memory_toolkit`` +and follows the ``azure.cosmos`` / ``azure.cosmos.aio`` convention. +""" + +from agent_memory_toolkit.aio.cosmos_memory_client import AsyncCosmosMemoryStore +from agent_memory_toolkit.aio.embeddings import AsyncEmbeddingsClient +from agent_memory_toolkit.aio.memory import AsyncAgentMemory +from agent_memory_toolkit.aio.processing import AsyncProcessingClient + +__all__ = [ + "AsyncAgentMemory", + "AsyncCosmosMemoryStore", + "AsyncEmbeddingsClient", + "AsyncProcessingClient", +] diff --git a/agent_memory_toolkit/aio/cosmos_memory_client.py b/agent_memory_toolkit/aio/cosmos_memory_client.py new file mode 100644 index 0000000..7251ef0 --- /dev/null +++ b/agent_memory_toolkit/aio/cosmos_memory_client.py @@ -0,0 +1,490 @@ +"""Async Cosmos DB client for the Agent Memory Toolkit. + +Provides :class:`AsyncCosmosMemoryStore` which uses +``azure.cosmos.aio.CosmosClient`` for non-blocking operations. + +Embedding generation is **not** this module's responsibility; callers must +supply pre-computed vectors to :meth:`vector_search`. +""" + +from __future__ import annotations + +import asyncio +import logging +from datetime import datetime, timezone +from typing import Any, Optional + +from agent_memory_toolkit._query_builder import _QueryBuilder +from agent_memory_toolkit._utils import ( + _build_memory_query_builder, + _container_policies, + _validate_connection, + _validate_hybrid_search, +) +from agent_memory_toolkit.exceptions import ( + CosmosNotConnectedError, + CosmosOperationError, + MemoryNotFoundError, +) +from agent_memory_toolkit.models import MemoryRecord + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Async client +# --------------------------------------------------------------------------- + + +class AsyncCosmosMemoryStore: + """Async Cosmos DB client for the Agent Memory Toolkit. + + Uses ``azure.cosmos.aio.CosmosClient`` for non-blocking operations. + """ + + def __init__( + self, + endpoint: str | None = None, + credential: Any = None, + database: str = "ai_memory", + container: str = "memories", + ) -> None: + self._endpoint = endpoint + self._credential = credential + self._database = database + self._container = container + self._cosmos_client: Any = None + self._container_client: Any = None + + # -- context manager ---------------------------------------------------- + + async def __aenter__(self) -> AsyncCosmosMemoryStore: + return self + + async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + await self.close() + + async def close(self) -> None: + """Close the underlying async Cosmos client.""" + if self._cosmos_client is not None: + await self._cosmos_client.close() + self._cosmos_client = None + self._container_client = None + logger.info("Async Cosmos client closed") + + # -- connection --------------------------------------------------------- + + async def connect(self) -> None: + """Create an async :class:`CosmosClient` and obtain the container. + + Raises + ------ + ConfigurationError + If required fields are missing. + CosmosOperationError + If the connection fails. + """ + _validate_connection( + self._endpoint, self._credential, self._database, self._container + ) + + try: + from azure.cosmos.aio import CosmosClient + + client = CosmosClient( + self._endpoint, credential=self._credential + ) + db = client.get_database_client(self._database) + container = db.get_container_client(self._container) + + self._cosmos_client = client + self._container_client = container + except Exception as exc: + raise CosmosOperationError( + f"Failed to connect to Cosmos DB (async): {exc}" + ) from exc + + logger.info( + "Async connected to Cosmos DB %s/%s", + self._database, + self._container, + ) + + async def create_store( + self, + embedding_dimensions: int = 1536, + embedding_data_type: str = "float32", + distance_function: str = "cosine", + full_text_language: str = "en-US", + autoscale_max_ru: int = 1000, + ) -> None: + """Create the database and container (async), then set the container handle. + + The container is provisioned with: + + * Hierarchical partition key ``[/user_id, /thread_id]`` + * ``quantizedFlat`` vector index on ``/embedding`` + * Full-text index on ``/content`` + * Autoscale throughput (max RU) + """ + _validate_connection( + self._endpoint, self._credential, self._database, self._container + ) + + try: + from azure.cosmos import PartitionKey, ThroughputProperties + from azure.cosmos.aio import CosmosClient + + client = CosmosClient( + self._endpoint, credential=self._credential + ) + + db = await client.create_database_if_not_exists( + id=self._database + ) + + partition_key = PartitionKey( + path=["/user_id", "/thread_id"], kind="MultiHash" + ) + + vec_policy, idx_policy, ft_policy = _container_policies( + embedding_dimensions=embedding_dimensions, + embedding_data_type=embedding_data_type, + distance_function=distance_function, + full_text_language=full_text_language, + ) + + container = await db.create_container_if_not_exists( + id=self._container, + partition_key=partition_key, + indexing_policy=idx_policy, + vector_embedding_policy=vec_policy, + full_text_policy=ft_policy, + offer_throughput=ThroughputProperties( + auto_scale_max_throughput=autoscale_max_ru, + ), + ) + self._cosmos_client = client + self._container_client = container + except Exception as exc: + raise CosmosOperationError( + f"Failed to create memory store (async): {exc}" + ) from exc + + logger.info( + "Async created memory store %s/%s", + self._database, + self._container, + ) + + def _require_connected(self) -> None: + """Raise if no active container client.""" + if self._container_client is None: + raise CosmosNotConnectedError() + + # -- upsert ------------------------------------------------------------- + + async def upsert(self, record: MemoryRecord) -> None: + """Upsert a single :class:`MemoryRecord`.""" + self._require_connected() + body = record.to_cosmos_dict() + try: + await self._container_client.upsert_item(body=body) + except Exception as exc: + raise CosmosOperationError( + f"Async upsert failed for record {record.id}: {exc}" + ) from exc + logger.info("Async upserted record %s", record.id) + + async def upsert_batch( + self, records: list[MemoryRecord], batch_size: int = 25 + ) -> None: + """Upsert multiple records using ``asyncio.gather`` in batches.""" + self._require_connected() + + for start in range(0, len(records), batch_size): + batch = records[start : start + batch_size] + tasks = [self.upsert(record) for record in batch] + await asyncio.gather(*tasks) + + logger.info("Async upserted batch of %d records", len(records)) + + # -- queries ------------------------------------------------------------ + + async def get_memories( + self, + memory_id: Optional[str] = None, + user_id: Optional[str] = None, + thread_id: Optional[str] = None, + role: Optional[str] = None, + memory_type: Optional[str] = None, + recent_k: Optional[int] = None, + ) -> list[dict[str, Any]]: + """Query memories with optional filters. + + Returns raw dicts. When *recent_k* is given the newest *k* + documents are returned in chronological (oldest-first) order. + """ + self._require_connected() + + qb = _build_memory_query_builder( + memory_id=memory_id, + user_id=user_id, + thread_id=thread_id, + role=role, + memory_type=memory_type, + ) + where = qb.build_where() + parameters = qb.get_parameters() + + if recent_k is not None: + parameters.append({"name": "@recent_k", "value": recent_k}) + query = f"SELECT TOP @recent_k * FROM c{where} ORDER BY c._ts DESC" + else: + query = f"SELECT * FROM c{where}" + + logger.debug("async get_memories query: %s", query) + + try: + items_iter = self._container_client.query_items( + query=query, + parameters=parameters or None, + ) + results = [item async for item in items_iter] + except Exception as exc: + raise CosmosOperationError( + f"async get_memories query failed: {exc}" + ) from exc + + if recent_k is not None: + results.reverse() + return results + + async def get_thread( + self, + thread_id: str, + user_id: Optional[str] = None, + memory_type: Optional[str] = None, + recent_k: Optional[int] = None, + ) -> list[dict[str, Any]]: + """Retrieve an entire thread, oldest-first.""" + self._require_connected() + + qb = _QueryBuilder() + qb.add_filter("c.thread_id", "@thread_id", thread_id) + qb.add_filter("c.user_id", "@user_id", user_id) + qb.add_filter("c.type", "@memory_type", memory_type) + + where = qb.build_where() + parameters = qb.get_parameters() + + query = f"SELECT * FROM c{where} ORDER BY c.created_at DESC" + logger.debug("async get_thread query: %s", query) + + try: + items_iter = self._container_client.query_items( + query=query, parameters=parameters + ) + items = [item async for item in items_iter] + except Exception as exc: + raise CosmosOperationError( + f"async get_thread query failed: {exc}" + ) from exc + + if recent_k is not None: + items = items[:recent_k] + items.reverse() + return items + + async def get_user_summary(self, user_id: str) -> list[dict[str, Any]]: + """Retrieve user-summary documents, newest-first.""" + self._require_connected() + + query = ( + "SELECT c.id, c.user_id, c.thread_id, c.role, c.type, " + "c.content, c.metadata, c.created_at " + "FROM c WHERE c.user_id = @user_id AND c.type = 'user_summary' " + "ORDER BY c.created_at DESC" + ) + parameters = [{"name": "@user_id", "value": user_id}] + logger.debug("async get_user_summary query: %s", query) + + try: + items_iter = self._container_client.query_items( + query=query, parameters=parameters + ) + return [item async for item in items_iter] + except Exception as exc: + raise CosmosOperationError( + f"async get_user_summary query failed: {exc}" + ) from exc + + # -- update / delete ---------------------------------------------------- + + async def update( + self, + memory_id: str, + content: Optional[str] = None, + role: Optional[str] = None, + memory_type: Optional[str] = None, + metadata: Optional[dict[str, Any]] = None, + ) -> None: + """Update fields on an existing memory document. + + Raises + ------ + MemoryNotFoundError + If the document does not exist. + CosmosOperationError + If the underlying Cosmos DB operation fails. + """ + self._require_connected() + + try: + items_iter = self._container_client.query_items( + query="SELECT * FROM c WHERE c.id = @id", + parameters=[{"name": "@id", "value": memory_id}], + ) + docs = [item async for item in items_iter] + except Exception as exc: + raise CosmosOperationError( + f"async update query failed: {exc}" + ) from exc + + if not docs: + raise MemoryNotFoundError(memory_id=memory_id) + + doc = docs[0] + if content is not None: + doc["content"] = content + if role is not None: + doc["role"] = role + if memory_type is not None: + doc["type"] = memory_type + if metadata is not None: + doc["metadata"] = metadata + doc["updated_at"] = datetime.now(timezone.utc).isoformat() + + try: + await self._container_client.replace_item(item=doc["id"], body=doc) + except Exception as exc: + raise CosmosOperationError( + f"async update replace failed for {memory_id}: {exc}" + ) from exc + + logger.info("Async updated record %s", memory_id) + + async def delete(self, memory_id: str, user_id: str, thread_id: str) -> None: + """Delete a memory document. + + Raises + ------ + MemoryNotFoundError + If no matching document exists. + CosmosOperationError + If the underlying Cosmos DB operation fails. + """ + self._require_connected() + + try: + items_iter = self._container_client.query_items( + query=( + "SELECT TOP 1 c.id FROM c WHERE c.id = @id " + "AND c.thread_id = @thread_id AND c.user_id = @user_id" + ), + parameters=[ + {"name": "@id", "value": memory_id}, + {"name": "@thread_id", "value": thread_id}, + {"name": "@user_id", "value": user_id}, + ], + ) + docs = [item async for item in items_iter] + except Exception as exc: + raise CosmosOperationError( + f"async delete lookup failed: {exc}" + ) from exc + + if not docs: + raise MemoryNotFoundError( + memory_id=memory_id, user_id=user_id, thread_id=thread_id + ) + + try: + await self._container_client.delete_item( + item=memory_id, partition_key=[user_id, thread_id] + ) + except Exception as exc: + raise CosmosOperationError( + f"async delete failed for {memory_id}: {exc}" + ) from exc + + logger.info("Async deleted record %s", memory_id) + + # -- vector search ------------------------------------------------------ + + async def vector_search( + self, + query_vector: list[float], + user_id: Optional[str] = None, + role: Optional[str] = None, + memory_type: Optional[str] = None, + thread_id: Optional[str] = None, + hybrid_search: bool = False, + search_terms: Optional[str] = None, + top_k: int = 5, + ) -> list[dict[str, Any]]: + """Run a vector (or hybrid) similarity search. + + Parameters + ---------- + query_vector : list[float] + Pre-computed embedding vector. + search_terms : str, optional + Raw text for the full-text component of a hybrid search. + Required when *hybrid_search* is ``True``. + """ + self._require_connected() + _validate_hybrid_search(hybrid_search, search_terms) + + qb = _build_memory_query_builder( + user_id=user_id, role=role, memory_type=memory_type, thread_id=thread_id + ) + where = qb.build_where() + parameters = qb.get_parameters() + + order_by = "ORDER BY VectorDistance(c.embedding, @embedding)" + if hybrid_search: + order_by = ( + "ORDER BY RANK RRF(" + "VectorDistance(c.embedding, @embedding), " + "FullTextScore(c.content, @key_terms)" + ")" + ) + + query = ( + f"SELECT TOP @top_k c.id, c.user_id, c.role, c.type, c.content, " + f"c.metadata, c.created_at " + f"FROM c{where} " + f"{order_by}" + ) + + parameters.extend( + [ + {"name": "@top_k", "value": top_k}, + {"name": "@embedding", "value": query_vector}, + ] + ) + if hybrid_search: + parameters.append({"name": "@key_terms", "value": search_terms or ""}) + + logger.debug("async vector_search query: %s", query) + + try: + items_iter = self._container_client.query_items( + query=query, parameters=parameters + ) + return [item async for item in items_iter] + except Exception as exc: + raise CosmosOperationError( + f"async vector_search failed: {exc}" + ) from exc diff --git a/agent_memory_toolkit/aio/embeddings.py b/agent_memory_toolkit/aio/embeddings.py new file mode 100644 index 0000000..3936364 --- /dev/null +++ b/agent_memory_toolkit/aio/embeddings.py @@ -0,0 +1,158 @@ +"""Async embedding client for the Agent Memory Toolkit. + +Provides :class:`AsyncEmbeddingsClient` that lazily initialises an +``openai.AsyncAzureOpenAI`` connection and generates embeddings via the +OpenAI API. +""" + +from __future__ import annotations + +import logging +from typing import Any + +from agent_memory_toolkit.exceptions import ConfigurationError, EmbeddingError + +logger = logging.getLogger(__name__) + +_TOKEN_SCOPE = "https://cognitiveservices.azure.com/.default" + + +class AsyncEmbeddingsClient: + """Async embedding client backed by Azure OpenAI. + + Supports the async context-manager protocol:: + + async with AsyncEmbeddingsClient(endpoint=..., credential=cred) as client: + vec = await client.generate("hello") + """ + + def __init__( + self, + endpoint: str | None = None, + credential: Any = None, + api_key: str | None = None, + model: str = "text-embedding-3-large", + dimensions: int | None = None, + api_version: str = "2024-12-01-preview", + ) -> None: + self._endpoint = endpoint + self._credential = credential + self._api_key = api_key + self._model = model + self._dimensions = dimensions + self._api_version = api_version + self._client: Any = None # openai.AsyncAzureOpenAI (lazy) + + # -- async context manager ---------------------------------------------- + + async def __aenter__(self) -> AsyncEmbeddingsClient: + return self + + async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + await self.close() + + async def close(self) -> None: + """Close the underlying HTTP client, if one has been created.""" + if self._client is not None: + await self._client.close() + self._client = None + + # -- internal helpers --------------------------------------------------- + + def _ensure_client(self) -> Any: + """Lazily create the ``AsyncAzureOpenAI`` client on first use.""" + if self._client is not None: + return self._client + + if not self._endpoint: + raise ConfigurationError( + "An embedding endpoint is required", parameter="endpoint" + ) + + from openai import AsyncAzureOpenAI + + if self._api_key: + self._client = AsyncAzureOpenAI( + api_version=self._api_version, + azure_endpoint=self._endpoint, + api_key=self._api_key, + ) + else: + if self._credential is None: + raise ConfigurationError( + "Either api_key or a TokenCredential is required for embeddings", + parameter="credential", + ) + from azure.identity.aio import get_bearer_token_provider + + token_provider = get_bearer_token_provider( + self._credential, _TOKEN_SCOPE + ) + self._client = AsyncAzureOpenAI( + api_version=self._api_version, + azure_endpoint=self._endpoint, + azure_ad_token_provider=token_provider, + ) + + return self._client + + def _build_kwargs(self, input_: str | list[str]) -> dict[str, Any]: + texts = [input_] if isinstance(input_, str) else input_ + logger.debug( + "Embedding request: model=%s, dimensions=%s, texts=%d", + self._model, + self._dimensions, + len(texts), + ) + kwargs: dict[str, Any] = {"input": texts, "model": self._model} + if self._dimensions: + kwargs["dimensions"] = self._dimensions + return kwargs + + # -- public API --------------------------------------------------------- + + async def generate(self, text: str) -> list[float]: + """Generate an embedding vector for *text*. + + Raises + ------ + ConfigurationError + If the endpoint or credentials are missing. + EmbeddingError + If the OpenAI API call fails. + """ + client = self._ensure_client() + kwargs = self._build_kwargs(text) + try: + response = await client.embeddings.create(**kwargs) + except Exception as exc: + raise EmbeddingError(f"Embedding generation failed: {exc}") from exc + return response.data[0].embedding + + async def generate_batch(self, texts: list[str]) -> list[list[float]]: + """Generate embeddings for multiple texts in a single API call. + + Returns a list of embedding vectors **in the same order** as *texts*. + + Raises + ------ + ConfigurationError + If the endpoint or credentials are missing. + EmbeddingError + If the OpenAI API call fails. + """ + if not texts: + return [] + + logger.info("Generating embeddings for batch of %d texts", len(texts)) + client = self._ensure_client() + kwargs = self._build_kwargs(texts) + try: + response = await client.embeddings.create(**kwargs) + except Exception as exc: + raise EmbeddingError( + f"Batch embedding generation failed: {exc}" + ) from exc + + sorted_data = sorted(response.data, key=lambda d: d.index) + return [item.embedding for item in sorted_data] diff --git a/agent_memory_toolkit/aio/memory.py b/agent_memory_toolkit/aio/memory.py new file mode 100644 index 0000000..535db97 --- /dev/null +++ b/agent_memory_toolkit/aio/memory.py @@ -0,0 +1,548 @@ +"""AsyncAgentMemory: async variant of AgentMemory. + +Thin async orchestrator that composes :class:`AsyncCosmosMemoryStore`, +:class:`AsyncEmbeddingsClient`, and :class:`AsyncProcessingClient`. +Local operations remain synchronous (in-memory list). + +Import from ``agent_memory_toolkit.aio``:: + + from agent_memory_toolkit.aio import AsyncAgentMemory +""" + +from __future__ import annotations + +import logging +from datetime import datetime, timezone +from typing import Any, Optional + +from agent_memory_toolkit.aio.cosmos_memory_client import AsyncCosmosMemoryStore +from agent_memory_toolkit.aio.embeddings import AsyncEmbeddingsClient +from agent_memory_toolkit.aio.processing import AsyncProcessingClient +from agent_memory_toolkit.exceptions import CosmosNotConnectedError, MemoryNotFoundError, ValidationError +from agent_memory_toolkit._utils import VALID_ROLES, VALID_TYPES, _make_memory, _resolve_embedding_dimensions +from agent_memory_toolkit.models import MemoryRecord + +logger = logging.getLogger(__name__) + + +class AsyncAgentMemory: + """Async variant of :class:`AgentMemory`. + + * Cosmos DB operations use ``azure.cosmos.aio`` + * Embeddings use ``openai.AsyncAzureOpenAI`` + * Processing uses ``aiohttp`` + * Local operations remain synchronous (in-memory list) + + Supports the async context-manager protocol:: + + async with AsyncAgentMemory() as mem: + await mem.connect_cosmos() + ... + + Parameters are identical to :class:`AgentMemory`. + """ + + def __init__( + self, + cosmos_endpoint: Optional[str] = None, + cosmos_credential: Optional[Any] = None, + cosmos_database: Optional[str] = None, + cosmos_container: Optional[str] = None, + ai_foundry_endpoint: Optional[str] = None, + ai_foundry_credential: Optional[Any] = None, + ai_foundry_api_key: Optional[str] = None, + embedding_model: str = "text-embedding-3-large", + embedding_dimensions: Optional[int] = None, + adf_endpoint: Optional[str] = None, + adf_key: Optional[str] = None, + use_default_credential: bool = True, + ) -> None: + # Local store + self.local_memory: list[dict[str, Any]] = [] + + # Store kwargs directly + self._cosmos_endpoint = cosmos_endpoint + self._cosmos_credential = cosmos_credential + self._cosmos_database = cosmos_database or "ai_memory" + self._cosmos_container = cosmos_container or "memories" + + self._ai_foundry_endpoint = ai_foundry_endpoint + self._ai_foundry_credential = ai_foundry_credential + self._ai_foundry_api_key = ai_foundry_api_key + self._embedding_model = embedding_model + self._embedding_dimensions = _resolve_embedding_dimensions(embedding_dimensions) + + self._adf_endpoint = adf_endpoint + self._adf_key = adf_key + + # Resolve credentials via async DefaultAzureCredential when needed + self._owns_credential = False + if use_default_credential: + needs_cosmos = self._cosmos_credential is None + needs_embed = self._ai_foundry_credential is None + if needs_cosmos or needs_embed: + try: + from azure.identity.aio import DefaultAzureCredential + _default = DefaultAzureCredential() + self._owns_credential = True + except ImportError: + _default = None + if needs_cosmos: + self._cosmos_credential = _default + if needs_embed: + self._ai_foundry_credential = _default + + # Sub-clients (cosmos store created on connect) + self._cosmos_store: Optional[AsyncCosmosMemoryStore] = None + self._embeddings_client = AsyncEmbeddingsClient( + endpoint=self._ai_foundry_endpoint, + credential=self._ai_foundry_credential, + api_key=self._ai_foundry_api_key, + model=self._embedding_model, + dimensions=self._embedding_dimensions, + ) + self._processing_client = AsyncProcessingClient( + endpoint=self._adf_endpoint, + key=self._adf_key, + ) + + logger.info("AsyncAgentMemory initialized") + + # ------------------------------------------------------------------ + # Async context manager + # ------------------------------------------------------------------ + + async def __aenter__(self) -> "AsyncAgentMemory": + return self + + async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + await self.close() + + async def close(self) -> None: + """Close all underlying async clients.""" + if self._cosmos_store is not None: + await self._cosmos_store.close() + await self._embeddings_client.close() + await self._processing_client.close() + if self._owns_credential and self._cosmos_credential is not None: + close = getattr(self._cosmos_credential, "close", None) + if close is not None: + await close() + logger.info("AsyncAgentMemory closed") + + # ------------------------------------------------------------------ + # Local operations (synchronous - in-memory list) + # ------------------------------------------------------------------ + + def add_local( + self, + user_id: str, + role: str, + content: str, + memory_type: str = "turn", + agent_id: Optional[str] = None, + metadata: Optional[dict[str, Any]] = None, + thread_id: Optional[str] = None, + ) -> None: + """Add a new memory to the local store.""" + memory = _make_memory( + user_id=user_id, + role=role, + content=content, + memory_type=memory_type, + agent_id=agent_id, + metadata=metadata, + thread_id=thread_id, + ) + self.local_memory.append(memory) + logger.debug("add_local id=%s role=%s type=%s", memory["id"], role, memory_type) + + def get_local( + self, + memory_id: Optional[str] = None, + user_id: Optional[str] = None, + role: Optional[str] = None, + memory_type: Optional[str] = None, + ) -> list[dict[str, Any]]: + """Retrieve memories from the local store. + + All filter parameters are optional. When none are provided every + memory is returned. Filters are combined with AND logic. + """ + logger.debug( + "get_local memory_id=%s user_id=%s role=%s type=%s", + memory_id, user_id, role, memory_type, + ) + results = self.local_memory + + if memory_id is not None: + results = [m for m in results if m["id"] == memory_id] + if user_id is not None: + results = [m for m in results if m["user_id"] == user_id] + if role is not None: + results = [m for m in results if m["role"] == role] + if memory_type is not None: + results = [m for m in results if m["type"] == memory_type] + + return results + + def update_local( + self, + memory_id: str, + content: Optional[str] = None, + role: Optional[str] = None, + memory_type: Optional[str] = None, + metadata: Optional[dict[str, Any]] = None, + ) -> None: + """Update an existing memory in the local store. + + Only the fields that are provided (not ``None``) will be updated. + + Raises + ------ + MemoryNotFoundError + If no memory with the given id exists. + ValidationError + If an invalid role or memory_type is provided. + """ + for memory in self.local_memory: + if memory["id"] == memory_id: + if content is not None: + memory["content"] = content + if role is not None: + if role not in VALID_ROLES: + raise ValidationError(f"role must be one of {VALID_ROLES}, got '{role}'") + memory["role"] = role + if memory_type is not None: + if memory_type not in VALID_TYPES: + raise ValidationError(f"type must be one of {VALID_TYPES}, got '{memory_type}'") + memory["type"] = memory_type + if metadata is not None: + memory["metadata"] = metadata + memory["updated_at"] = datetime.now(timezone.utc).isoformat() + return + + raise MemoryNotFoundError(memory_id=memory_id) + + def delete_local(self, memory_id: str) -> None: + """Delete a memory from the local store by id. + + Raises + ------ + MemoryNotFoundError + If no memory with the given id exists. + """ + for i, memory in enumerate(self.local_memory): + if memory["id"] == memory_id: + self.local_memory.pop(i) + return + + raise MemoryNotFoundError(memory_id=memory_id) + + # ------------------------------------------------------------------ + # Cosmos DB connection (async) + # ------------------------------------------------------------------ + + async def connect_cosmos( + self, + endpoint: Optional[str] = None, + credential: Optional[Any] = None, + database: Optional[str] = None, + container: Optional[str] = None, + ) -> None: + """Establish an async connection to a Cosmos DB container. + + Parameters override whatever was set in ``__init__``. After this + call the Cosmos CRUD methods are ready to use. + """ + self._cosmos_endpoint = endpoint or self._cosmos_endpoint + self._cosmos_credential = credential or self._cosmos_credential + self._cosmos_database = database or self._cosmos_database + self._cosmos_container = container or self._cosmos_container + + self._cosmos_store = AsyncCosmosMemoryStore( + endpoint=self._cosmos_endpoint, + credential=self._cosmos_credential, + database=self._cosmos_database, + container=self._cosmos_container, + ) + await self._cosmos_store.connect() + + async def create_memory_store( + self, + database: Optional[str] = None, + container: Optional[str] = None, + endpoint: Optional[str] = None, + credential: Optional[Any] = None, + embedding_dimensions: Optional[int] = None, + embedding_data_type: Optional[str] = None, + distance_function: Optional[str] = None, + full_text_language: Optional[str] = None, + ) -> None: + """Create the Cosmos DB database and container for memories (async). + + After successful creation the instance is connected and ready + for CRUD operations. + """ + self._cosmos_endpoint = endpoint or self._cosmos_endpoint + self._cosmos_credential = credential or self._cosmos_credential + self._cosmos_database = database or self._cosmos_database + self._cosmos_container = container or self._cosmos_container + + self._cosmos_store = AsyncCosmosMemoryStore( + endpoint=self._cosmos_endpoint, + credential=self._cosmos_credential, + database=self._cosmos_database, + container=self._cosmos_container, + ) + await self._cosmos_store.create_store( + embedding_dimensions=embedding_dimensions or self._embedding_dimensions or 1536, + embedding_data_type=embedding_data_type or "float32", + distance_function=distance_function or "cosine", + full_text_language=full_text_language or "en-US", + ) + + def _require_cosmos(self) -> None: + """Raise if Cosmos DB is not connected.""" + if self._cosmos_store is None: + raise CosmosNotConnectedError() + self._cosmos_store._require_connected() + + # ------------------------------------------------------------------ + # Cosmos DB operations (async) + # ------------------------------------------------------------------ + + async def add_cosmos( + self, + user_id: str, + role: str, + content: str, + memory_type: str = "turn", + metadata: Optional[dict[str, Any]] = None, + thread_id: Optional[str] = None, + ) -> None: + """Add a memory to Cosmos DB.""" + self._require_cosmos() + kwargs: dict[str, Any] = { + "user_id": user_id, + "role": role, + "content": content, + "memory_type": memory_type, + "metadata": metadata or {}, + } + if thread_id is not None: + kwargs["thread_id"] = thread_id + record = MemoryRecord(**kwargs) + await self._cosmos_store.upsert(record) + logger.info("add_cosmos id=%s role=%s type=%s", record.id, role, memory_type) + + async def push_to_cosmos(self, batch_size: int = 25) -> None: + """Insert all local memories into Cosmos DB in concurrent batches. + + Each local memory is inserted as-is, preserving its existing + ``id``, ``thread_id``, timestamps, and metadata. + """ + self._require_cosmos() + if batch_size <= 0: + raise ValueError("batch_size must be greater than 0") + logger.info( + "push_to_cosmos count=%d batch_size=%d", + len(self.local_memory), + batch_size, + ) + records = [MemoryRecord.from_cosmos_dict(dict(m)) for m in self.local_memory] + await self._cosmos_store.upsert_batch(records, batch_size=batch_size) + + async def get_memories( + self, + memory_id: Optional[str] = None, + user_id: Optional[str] = None, + thread_id: Optional[str] = None, + role: Optional[str] = None, + memory_type: Optional[str] = None, + recent_k: Optional[int] = None, + ) -> list[dict[str, Any]]: + """Retrieve memories from Cosmos DB with optional filters.""" + self._require_cosmos() + logger.debug( + "get_memories filters: memory_id=%s user_id=%s thread_id=%s role=%s type=%s recent_k=%s", + memory_id, user_id, thread_id, role, memory_type, recent_k, + ) + results = await self._cosmos_store.get_memories( + memory_id=memory_id, + user_id=user_id, + thread_id=thread_id, + role=role, + memory_type=memory_type, + recent_k=recent_k, + ) + if not results: + logger.warning("get_memories returned empty results") + return results + + async def update_cosmos( + self, + memory_id: str, + content: Optional[str] = None, + role: Optional[str] = None, + memory_type: Optional[str] = None, + metadata: Optional[dict[str, Any]] = None, + ) -> None: + """Update a memory in Cosmos DB.""" + self._require_cosmos() + await self._cosmos_store.update( + memory_id=memory_id, + content=content, + role=role, + memory_type=memory_type, + metadata=metadata, + ) + + async def delete_cosmos(self, memory_id: str, thread_id: str, user_id: str) -> None: + """Delete a memory from Cosmos DB.""" + self._require_cosmos() + await self._cosmos_store.delete( + memory_id=memory_id, + user_id=user_id, + thread_id=thread_id, + ) + + async def search_cosmos( + self, + search_terms: str, + memory_id: Optional[str] = None, + user_id: Optional[str] = None, + role: Optional[str] = None, + memory_type: Optional[str] = None, + thread_id: Optional[str] = None, + hybrid_search: bool = False, + top_k: int = 5, + ) -> list[dict[str, Any]]: + """Search memories in Cosmos DB using vector similarity. + + 1. Embeds *search_terms* via the configured embedding model. + 2. Runs a vector similarity query against the Cosmos DB container. + 3. Optionally filters by the remaining keyword parameters. + 4. Returns up to *top_k* results ordered by similarity. + """ + self._require_cosmos() + logger.info( + "search_cosmos terms_len=%d top_k=%d hybrid_search=%s", + len(search_terms), + top_k, + hybrid_search, + ) + logger.debug( + "search_cosmos search_terms=%s", + search_terms[:50] + "..." if len(search_terms) > 50 else search_terms, + ) + query_vector = await self._embeddings_client.generate(search_terms) + results = await self._cosmos_store.vector_search( + query_vector=query_vector, + user_id=user_id, + role=role, + memory_type=memory_type, + thread_id=thread_id, + hybrid_search=hybrid_search, + search_terms=search_terms, + top_k=top_k, + ) + # Post-filter by memory_id (not supported directly by vector_search) + if memory_id is not None: + results = [r for r in results if r.get("id") == memory_id] + if not results: + logger.warning( + "search_cosmos returned empty results (terms_len=%d)", + len(search_terms), + ) + return results + + async def get_thread( + self, + thread_id: str, + user_id: Optional[str] = None, + memory_type: Optional[str] = None, + recent_k: Optional[int] = None, + ) -> list[dict[str, Any]]: + """Retrieve an entire thread from Cosmos DB. + + Returns memories sorted in chronological order (oldest first). + """ + self._require_cosmos() + return await self._cosmos_store.get_thread( + thread_id=thread_id, + user_id=user_id, + memory_type=memory_type, + recent_k=recent_k, + ) + + async def get_user_summary(self, user_id: str) -> list[dict[str, Any]]: + """Retrieve user summary documents from Cosmos DB, newest first.""" + self._require_cosmos() + return await self._cosmos_store.get_user_summary(user_id=user_id) + + # ------------------------------------------------------------------ + # Processing (Azure Durable Functions, async) + # ------------------------------------------------------------------ + + async def generate_thread_summary( + self, + user_id: str, + thread_id: str, + recent_k: Optional[int] = None, + poll_interval: float = 2.0, + timeout: float = 120.0, + ) -> dict[str, Any]: + """Trigger the Azure Durable Function to generate a thread summary (async).""" + logger.info( + "generate_thread_summary started user_id=%s thread_id=%s", + user_id, + thread_id, + ) + return await self._processing_client.generate_thread_summary( + user_id=user_id, + thread_id=thread_id, + recent_k=recent_k, + poll_interval=poll_interval, + timeout=timeout, + ) + + async def extract_facts( + self, + user_id: str, + thread_id: str, + recent_k: Optional[int] = None, + poll_interval: float = 2.0, + timeout: float = 120.0, + ) -> dict[str, Any]: + """Trigger the Azure Durable Function to extract facts (async).""" + logger.info( + "extract_facts started user_id=%s thread_id=%s", + user_id, + thread_id, + ) + return await self._processing_client.extract_facts( + user_id=user_id, + thread_id=thread_id, + recent_k=recent_k, + poll_interval=poll_interval, + timeout=timeout, + ) + + async def generate_user_summary( + self, + user_id: str, + thread_ids: Optional[list[str]] = None, + recent_k: Optional[int] = None, + poll_interval: float = 2.0, + timeout: float = 120.0, + ) -> dict[str, Any]: + """Trigger the Azure Durable Function to generate a cross-thread user summary (async).""" + logger.info("generate_user_summary started user_id=%s", user_id) + return await self._processing_client.generate_user_summary( + user_id=user_id, + thread_ids=thread_ids, + recent_k=recent_k, + poll_interval=poll_interval, + timeout=timeout, + ) diff --git a/agent_memory_toolkit/aio/processing.py b/agent_memory_toolkit/aio/processing.py new file mode 100644 index 0000000..c76f4df --- /dev/null +++ b/agent_memory_toolkit/aio/processing.py @@ -0,0 +1,216 @@ +"""Async Azure Durable Functions client for the Agent Memory Toolkit. + +Provides :class:`AsyncProcessingClient` (asyncio + aiohttp) that +encapsulates the HTTP-start → poll-until-done lifecycle of Durable +Functions orchestrations. +""" + +from __future__ import annotations + +import logging +from typing import Any + +from agent_memory_toolkit.exceptions import ( + ConfigurationError, + OrchestrationTimeoutError, + ProcessingError, +) + +logger = logging.getLogger(__name__) + +_ORCHESTRATOR_PATH = "/orchestrators/memory_orchestrator" +_TERMINAL_STATUSES = frozenset(("Completed", "Failed", "Terminated")) + + +class AsyncProcessingClient: + """Async Azure Durable Functions client using aiohttp.""" + + def __init__( + self, + endpoint: str | None = None, + key: str | None = None, + poll_interval: float = 2.0, + timeout: float = 120.0, + ) -> None: + self._endpoint = endpoint + self._key = key + self._poll_interval = poll_interval + self._timeout = timeout + self._session: Any = None # aiohttp.ClientSession, lazily created + + # -- async context manager ---------------------------------------------- + + async def __aenter__(self) -> AsyncProcessingClient: + return self + + async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + await self.close() + + async def close(self) -> None: + """Close the underlying HTTP session.""" + if self._session is not None and not self._session.closed: + await self._session.close() + self._session = None + + # -- internal helpers --------------------------------------------------- + + async def _get_session(self) -> Any: + """Return the shared :class:`aiohttp.ClientSession`, creating it on first use.""" + if self._session is None or self._session.closed: + import aiohttp + + self._session = aiohttp.ClientSession() + return self._session + + # -- core --------------------------------------------------------------- + + async def invoke_orchestrator( + self, + payload: dict[str, Any], + poll_interval: float | None = None, + timeout: float | None = None, + ) -> dict[str, Any]: + """Start an orchestration and poll until it reaches a terminal state. + + Parameters + ---------- + payload: + JSON body sent to the orchestrator HTTP-start endpoint. + poll_interval: + Seconds between status polls. Falls back to constructor default. + timeout: + Maximum seconds to wait. Falls back to constructor default. + + Returns + ------- + dict + The full status response from the orchestration. + + Raises + ------ + ConfigurationError + If ``endpoint`` is not set. + ProcessingError + If the orchestration finishes with ``runtimeStatus == "Failed"``. + OrchestrationTimeoutError + If polling exceeds *timeout*. + """ + import asyncio + + if not self._endpoint: + raise ConfigurationError( + "Processing endpoint is required to invoke orchestrations", + parameter="endpoint", + ) + + poll_interval = poll_interval if poll_interval is not None else self._poll_interval + timeout = timeout if timeout is not None else self._timeout + + url = self._endpoint.rstrip("/") + _ORCHESTRATOR_PATH + if self._key: + url += f"?code={self._key}" + + logger.debug("POST %s with payload %s", url, payload) + + import aiohttp + + session = await self._get_session() + + try: + async with session.post(url, json=payload) as resp: + resp.raise_for_status() + start_response: dict[str, Any] = await resp.json() + except aiohttp.ClientError as exc: + raise ProcessingError( + f"Failed to start orchestration: {exc}" + ) from exc + + status_url = start_response.get("statusQueryGetUri") + if not status_url: + return start_response + + logger.info( + "Orchestration started (instance=%s), polling for completion", + start_response.get("id"), + ) + + loop = asyncio.get_running_loop() + deadline = loop.time() + timeout + while loop.time() < deadline: + await asyncio.sleep(poll_interval) + try: + async with session.get(status_url) as resp: + resp.raise_for_status() + status: dict[str, Any] = await resp.json() + except aiohttp.ClientError as exc: + raise ProcessingError( + f"Failed to poll orchestration status: {exc}" + ) from exc + + runtime_status = status.get("runtimeStatus", "") + logger.debug("Poll runtimeStatus=%s", runtime_status) + + if runtime_status in _TERMINAL_STATUSES: + if runtime_status == "Failed": + error_detail = status.get("output") or status.get("customStatus") + raise ProcessingError( + f"Orchestration failed: {error_detail}" + ) + logger.info("Orchestration completed with status=%s", runtime_status) + return status + + raise OrchestrationTimeoutError(timeout=timeout, status_url=status_url) + + # -- convenience wrappers ----------------------------------------------- + + async def generate_thread_summary( + self, + user_id: str, + thread_id: str, + recent_k: int | None = None, + **kwargs: Any, + ) -> dict[str, Any]: + """Generate a summary for a single thread.""" + payload: dict[str, Any] = { + "user_id": user_id, + "thread_id": thread_id, + "thread_summary_only": True, + } + if recent_k is not None: + payload["recent_k"] = recent_k + return await self.invoke_orchestrator(payload, **kwargs) + + async def extract_facts( + self, + user_id: str, + thread_id: str, + recent_k: int | None = None, + **kwargs: Any, + ) -> dict[str, Any]: + """Extract factual knowledge from a thread.""" + payload: dict[str, Any] = { + "user_id": user_id, + "thread_id": thread_id, + "extract_facts_only": True, + } + if recent_k is not None: + payload["recent_k"] = recent_k + return await self.invoke_orchestrator(payload, **kwargs) + + async def generate_user_summary( + self, + user_id: str, + thread_ids: list[str] | None = None, + recent_k: int | None = None, + **kwargs: Any, + ) -> dict[str, Any]: + """Generate a cross-thread summary for a user.""" + payload: dict[str, Any] = { + "user_id": user_id, + "user_summary_only": True, + } + if thread_ids is not None: + payload["thread_ids"] = thread_ids + if recent_k is not None: + payload["recent_k"] = recent_k + return await self.invoke_orchestrator(payload, **kwargs) diff --git a/agent_memory_toolkit/async_memory.py b/agent_memory_toolkit/async_memory.py deleted file mode 100644 index fee88a0..0000000 --- a/agent_memory_toolkit/async_memory.py +++ /dev/null @@ -1,760 +0,0 @@ -"""AsyncAgentMemory: Async version of AgentMemory using azure.cosmos.aio and AsyncAzureOpenAI.""" - -import uuid, os -from datetime import datetime, timezone -from typing import Any, Optional - -from agent_memory_toolkit.memory import VALID_ROLES, VALID_TYPES, _make_memory - - -class AsyncAgentMemory: - """Async variant of :class:`AgentMemory`. - - * Cosmos DB operations use ``azure.cosmos.aio`` - * Embeddings use ``openai.AsyncAzureOpenAI`` - * The ``generate_thread_summary`` method uses ``aiohttp`` for non-blocking HTTP - * Local operations remain synchronous (in-memory list) - - Parameters are identical to :class:`AgentMemory`. - """ - - def __init__( - self, - cosmos_endpoint: Optional[str] = None, - cosmos_credential: Optional["TokenCredential"] = None, - cosmos_database: Optional[str] = None, - cosmos_container: Optional[str] = None, - ai_foundry_endpoint: Optional[str] = None, - ai_foundry_credential: Optional["TokenCredential"] = None, - ai_foundry_api_key: Optional[str] = None, - embedding_model: str = "text-embedding-3-large", - embedding_dimensions: Optional[int] = None, - adf_endpoint: Optional[str] = None, - adf_key: Optional[str] = None, - use_default_credential: bool = True, - ) -> None: - self.local_memory: list[dict[str, Any]] = [] - - if use_default_credential and (cosmos_credential is None or ai_foundry_credential is None): - try: - from azure.identity.aio import DefaultAzureCredential - _default = DefaultAzureCredential() - except ImportError: - _default = None - - if cosmos_credential is None: - cosmos_credential = _default - if ai_foundry_credential is None: - ai_foundry_credential = _default - - self.cosmos_endpoint = cosmos_endpoint - self.cosmos_credential = cosmos_credential - self.cosmos_database = cosmos_database - self.cosmos_container = cosmos_container - self._cosmos_client = None - self._cosmos_container_client = None - - self.ai_foundry_endpoint = ai_foundry_endpoint - self.ai_foundry_credential = ai_foundry_credential - self.ai_foundry_api_key = ai_foundry_api_key - self.embedding_model = embedding_model - self.embedding_dimensions = embedding_dimensions or int( - _os.environ.get("EMBEDDING_DIMENSIONS", "0") or "0" - ) or None - self._embeddings_client = None - - self.adf_endpoint = adf_endpoint - self.adf_key = adf_key - - # ------------------------------------------------------------------ - # Local operations (synchronous – in-memory list) - # ------------------------------------------------------------------ - - def add_local( - self, - user_id: str, - role: str, - content: str, - memory_type: str = "turn", - agent_id: Optional[str] = None, - metadata: Optional[dict[str, Any]] = None, - thread_id: Optional[str] = None, - ) -> None: - memory = _make_memory( - user_id=user_id, role=role, content=content, - memory_type=memory_type, agent_id=agent_id, - metadata=metadata, thread_id=thread_id, - ) - self.local_memory.append(memory) - - def get_local( - self, - memory_id: Optional[str] = None, - user_id: Optional[str] = None, - role: Optional[str] = None, - memory_type: Optional[str] = None, - ) -> list[dict[str, Any]]: - results = self.local_memory - if memory_id is not None: - results = [m for m in results if m["id"] == memory_id] - if user_id is not None: - results = [m for m in results if m["user_id"] == user_id] - if role is not None: - results = [m for m in results if m["role"] == role] - if memory_type is not None: - results = [m for m in results if m["type"] == memory_type] - return results - - def update_local( - self, - memory_id: str, - content: Optional[str] = None, - role: Optional[str] = None, - memory_type: Optional[str] = None, - metadata: Optional[dict[str, Any]] = None, - ) -> None: - for memory in self.local_memory: - if memory["id"] == memory_id: - if content is not None: - memory["content"] = content - if role is not None: - if role not in VALID_ROLES: - raise ValueError(f"role must be one of {VALID_ROLES}, got '{role}'") - memory["role"] = role - if memory_type is not None: - if memory_type not in VALID_TYPES: - raise ValueError(f"type must be one of {VALID_TYPES}, got '{memory_type}'") - memory["type"] = memory_type - if metadata is not None: - memory["metadata"] = metadata - memory["updated_at"] = datetime.now(timezone.utc).isoformat() - return - raise KeyError(f"No memory found with id '{memory_id}'") - - def delete_local(self, memory_id: str) -> None: - for i, memory in enumerate(self.local_memory): - if memory["id"] == memory_id: - self.local_memory.pop(i) - return - raise KeyError(f"No memory found with id '{memory_id}'") - - # ------------------------------------------------------------------ - # Cosmos DB connection (async) - # ------------------------------------------------------------------ - - async def connect_cosmos( - self, - endpoint: Optional[str] = None, - credential: Optional["TokenCredential"] = None, - database: Optional[str] = None, - container: Optional[str] = None, - ) -> None: - """Establish an async connection to a Cosmos DB container.""" - from azure.cosmos.aio import CosmosClient - - self.cosmos_endpoint = endpoint or self.cosmos_endpoint - self.cosmos_credential = credential or self.cosmos_credential - self.cosmos_database = database or self.cosmos_database - self.cosmos_container = container or self.cosmos_container - - if not self.cosmos_endpoint: - raise ValueError("cosmos_endpoint is required") - if not self.cosmos_credential: - raise ValueError("cosmos_credential is required") - if not self.cosmos_database: - raise ValueError("cosmos_database is required") - if not self.cosmos_container: - raise ValueError("cosmos_container is required") - - self._cosmos_client = CosmosClient( - self.cosmos_endpoint, credential=self.cosmos_credential, - ) - db = self._cosmos_client.get_database_client(self.cosmos_database) - self._cosmos_container_client = db.get_container_client(self.cosmos_container) - - async def create_memory_store( - self, - database: Optional[str] = None, - container: Optional[str] = None, - endpoint: Optional[str] = None, - credential: Optional["TokenCredential"] = None, - embedding_dimensions: Optional[int] = None, - embedding_data_type: Optional[str] = None, - distance_function: Optional[str] = None, - full_text_language: Optional[str] = None, - ) -> None: - """Create the Cosmos DB database and container (async).""" - import os as _os - from azure.cosmos.aio import CosmosClient - from azure.cosmos import PartitionKey, ThroughputProperties - - self.cosmos_endpoint = endpoint or self.cosmos_endpoint - self.cosmos_credential = credential or self.cosmos_credential - self.cosmos_database = database or self.cosmos_database - self.cosmos_container = container or self.cosmos_container - - if not self.cosmos_endpoint: - raise ValueError("cosmos_endpoint is required") - if not self.cosmos_credential: - raise ValueError("cosmos_credential is required") - if not self.cosmos_database: - raise ValueError("cosmos_database is required") - if not self.cosmos_container: - raise ValueError("cosmos_container is required") - - embedding_dimensions = embedding_dimensions or int( - _os.environ.get("EMBEDDING_DIMENSIONS", "3072") - ) - embedding_data_type = ( - embedding_data_type or _os.environ.get("EMBEDDING_DATA_TYPE", "float32") - ) - distance_function = ( - distance_function or _os.environ.get("EMBEDDING_DISTANCE_FUNCTION", "cosine") - ) - full_text_language = ( - full_text_language or _os.environ.get("FULL_TEXT_LANGUAGE", "en-US") - ) - autoscale_max_ru = int( - _os.environ.get("COSMOS_DB_AUTOSCALE_MAX_RU", "1000") - ) - - self._cosmos_client = CosmosClient( - self.cosmos_endpoint, credential=self.cosmos_credential, - ) - - db = await self._cosmos_client.create_database_if_not_exists(id=self.cosmos_database) - - partition_key = PartitionKey(path=["/user_id", "/thread_id"], kind="MultiHash") - - vector_embedding_policy = { - "vectorEmbeddings": [{ - "path": "/embedding", - "dataType": embedding_data_type, - "distanceFunction": distance_function, - "dimensions": embedding_dimensions, - }] - } - indexing_policy = { - "includedPaths": [{"path": "/*"}], - "excludedPaths": [{"path": "/embedding/*"}], - "vectorIndexes": [{"path": "/embedding", "type": "quantizedFlat"}], - "fullTextIndexes": [{"path": "/content"}], - } - full_text_policy = { - "defaultLanguage": full_text_language, - "fullTextPaths": [{"path": "/content", "language": full_text_language}], - } - - container_obj = await db.create_container_if_not_exists( - id=self.cosmos_container, - partition_key=partition_key, - indexing_policy=indexing_policy, - vector_embedding_policy=vector_embedding_policy, - full_text_policy=full_text_policy, - offer_throughput=ThroughputProperties( - auto_scale_max_throughput=autoscale_max_ru, - ), - ) - self._cosmos_container_client = container_obj - - def _require_cosmos(self): - if self._cosmos_container_client is None: - raise RuntimeError("Cosmos DB is not connected. Call await connect_cosmos() first.") - - # ------------------------------------------------------------------ - # Embeddings helper (async) - # ------------------------------------------------------------------ - - async def _get_embedding(self, text: str) -> list[float]: - """Generate a vector embedding using AsyncAzureOpenAI.""" - if self._embeddings_client is None: - from openai import AsyncAzureOpenAI - - if not self.ai_foundry_endpoint: - raise ValueError("ai_foundry_endpoint is required for embeddings") - - if self.ai_foundry_api_key: - self._embeddings_client = AsyncAzureOpenAI( - api_version="2024-12-01-preview", - azure_endpoint=self.ai_foundry_endpoint, - api_key=self.ai_foundry_api_key, - ) - else: - if not self.ai_foundry_credential: - raise ValueError( - "ai_foundry_credential or ai_foundry_api_key is required for embeddings" - ) - from azure.identity.aio import get_bearer_token_provider - - token_provider = get_bearer_token_provider( - self.ai_foundry_credential, - "https://cognitiveservices.azure.com/.default", - ) - self._embeddings_client = AsyncAzureOpenAI( - api_version="2024-12-01-preview", - azure_endpoint=self.ai_foundry_endpoint, - azure_ad_token_provider=token_provider, - ) - - kwargs: dict[str, Any] = { - "input": [text], - "model": self.embedding_model, - } - if self.embedding_dimensions: - kwargs["dimensions"] = self.embedding_dimensions - response = await self._embeddings_client.embeddings.create(**kwargs) - return response.data[0].embedding - - # ------------------------------------------------------------------ - # Cosmos DB operations (async) - # ------------------------------------------------------------------ - - async def add_cosmos( - self, - user_id: str, - role: str, - content: str, - memory_type: str = "turn", - metadata: Optional[dict[str, Any]] = None, - thread_id: Optional[str] = None, - ) -> None: - self._require_cosmos() - memory = _make_memory( - user_id=user_id, role=role, content=content, - memory_type=memory_type, metadata=metadata, thread_id=thread_id, - ) - await self._cosmos_container_client.upsert_item(body=memory) - - async def push_to_cosmos(self, batch_size: int = 25) -> None: - """Insert all local memories into Cosmos DB in concurrent batches.""" - import asyncio - - self._require_cosmos() - - if batch_size <= 0: - raise ValueError("batch_size must be greater than 0") - - for start in range(0, len(self.local_memory), batch_size): - batch = self.local_memory[start:start + batch_size] - tasks = [ - asyncio.create_task( - self._cosmos_container_client.upsert_item(body=dict(memory)) - ) - for memory in batch - ] - if tasks: - done, pending = await asyncio.wait(tasks) - for task in pending: - task.cancel() - for task in done: - task.result() - - async def get_memories( - self, - memory_id: Optional[str] = None, - user_id: Optional[str] = None, - thread_id: Optional[str] = None, - role: Optional[str] = None, - memory_type: Optional[str] = None, - recent_k: Optional[int] = None, - ) -> list[dict[str, Any]]: - self._require_cosmos() - - conditions: list[str] = [] - parameters: list[dict[str, Any]] = [] - - if memory_id is not None: - conditions.append("c.id = @memory_id") - parameters.append({"name": "@memory_id", "value": memory_id}) - if user_id is not None: - conditions.append("c.user_id = @user_id") - parameters.append({"name": "@user_id", "value": user_id}) - if thread_id is not None: - conditions.append("c.thread_id = @thread_id") - parameters.append({"name": "@thread_id", "value": thread_id}) - if role is not None: - conditions.append("c.role = @role") - parameters.append({"name": "@role", "value": role}) - if memory_type is not None: - conditions.append("c.type = @memory_type") - parameters.append({"name": "@memory_type", "value": memory_type}) - - where = (" WHERE " + " AND ".join(conditions)) if conditions else "" - - if recent_k is not None: - top_clause = "TOP @recent_k " - parameters.append({"name": "@recent_k", "value": recent_k}) - query = f"SELECT {top_clause}* FROM c{where} ORDER BY c._ts DESC" - else: - query = f"SELECT * FROM c{where}" - - items = self._cosmos_container_client.query_items( - query=query, - parameters=parameters or None, - ) - results = [item async for item in items] - if recent_k is not None: - results.reverse() - return results - - async def update_cosmos( - self, - memory_id: str, - content: Optional[str] = None, - role: Optional[str] = None, - memory_type: Optional[str] = None, - metadata: Optional[dict[str, Any]] = None, - ) -> None: - self._require_cosmos() - - results = self._cosmos_container_client.query_items( - query="SELECT * FROM c WHERE c.id = @id", - parameters=[{"name": "@id", "value": memory_id}] - ) - docs = [item async for item in results] - if not docs: - raise KeyError(f"No memory found with id '{memory_id}'") - - doc = docs[0] - if content is not None: - doc["content"] = content - if role is not None: - if role not in VALID_ROLES: - raise ValueError(f"role must be one of {VALID_ROLES}, got '{role}'") - doc["role"] = role - if memory_type is not None: - if memory_type not in VALID_TYPES: - raise ValueError(f"type must be one of {VALID_TYPES}, got '{memory_type}'") - doc["type"] = memory_type - if metadata is not None: - doc["metadata"] = metadata - doc["updated_at"] = datetime.now(timezone.utc).isoformat() - - await self._cosmos_container_client.replace_item(item=doc["id"], body=doc) - - async def delete_cosmos(self, memory_id: str, thread_id: str, user_id: str) -> None: - self._require_cosmos() - - results = self._cosmos_container_client.query_items( - query=( - "SELECT TOP 1 c.id FROM c WHERE c.id = @id " - "AND c.thread_id = @thread_id AND c.user_id = @user_id" - ), - parameters=[ - {"name": "@id", "value": memory_id}, - {"name": "@thread_id", "value": thread_id}, - {"name": "@user_id", "value": user_id}, - ] - ) - docs = [item async for item in results] - if not docs: - raise KeyError( - f"No memory found with id '{memory_id}' for user_id '{user_id}' and thread_id '{thread_id}'" - ) - - await self._cosmos_container_client.delete_item( - item=memory_id, partition_key=[user_id, thread_id], - ) - - async def search_cosmos( - self, - search_terms: str, - memory_id: Optional[str] = None, - user_id: Optional[str] = None, - role: Optional[str] = None, - memory_type: Optional[str] = None, - thread_id: Optional[str] = None, - hybrid_search: bool = False, - top_k: int = 5, - ) -> list[dict[str, Any]]: - self._require_cosmos() - - query_vector = await self._get_embedding(search_terms) - - conditions: list[str] = [] - parameters: list[dict[str, Any]] = [] - if memory_id is not None: - conditions.append("c.id = @memory_id") - parameters.append({"name": "@memory_id", "value": memory_id}) - if user_id is not None: - conditions.append("c.user_id = @user_id") - parameters.append({"name": "@user_id", "value": user_id}) - if role is not None: - conditions.append("c.role = @role") - parameters.append({"name": "@role", "value": role}) - if memory_type is not None: - conditions.append("c.type = @memory_type") - parameters.append({"name": "@memory_type", "value": memory_type}) - if thread_id is not None: - conditions.append("c.thread_id = @thread_id") - parameters.append({"name": "@thread_id", "value": thread_id}) - - where = (" WHERE " + " AND ".join(conditions)) if conditions else "" - - order_by = "ORDER BY VectorDistance(c.embedding, @embedding)" - if hybrid_search: - order_by = ( - "ORDER BY RANK RRF(" - "VectorDistance(c.embedding, @embedding), " - "FullTextScore(c.content, @key_terms)" - ")" - ) - - query = ( - f"SELECT TOP @top_k c.id, c.user_id, c.role, c.type, c.content, " - f"c.metadata, c.created_at " - f"FROM c{where} " - f"{order_by}" - ) - parameters.extend([ - {"name": "@top_k", "value": top_k}, - {"name": "@embedding", "value": query_vector}, - ]) - if hybrid_search: - parameters.append({"name": "@key_terms", "value": search_terms}) - - items = self._cosmos_container_client.query_items( - query=query, parameters=parameters - ) - return [item async for item in items] - - async def get_thread( - self, - thread_id: str, - user_id: Optional[str] = None, - memory_type: Optional[str] = None, - recent_k: Optional[int] = None, - ) -> list[dict[str, Any]]: - self._require_cosmos() - - conditions: list[str] = ["c.thread_id = @thread_id"] - parameters: list[dict[str, Any]] = [ - {"name": "@thread_id", "value": thread_id}, - ] - if user_id is not None: - conditions.append("c.user_id = @user_id") - parameters.append({"name": "@user_id", "value": user_id}) - - if memory_type is not None: - conditions.append("c.type = @memory_type") - parameters.append({"name": "@memory_type", "value": memory_type}) - - where = " WHERE " + " AND ".join(conditions) - query = f"SELECT * FROM c{where} ORDER BY c.created_at DESC" - - results = self._cosmos_container_client.query_items( - query=query, parameters=parameters - ) - items = [item async for item in results] - - if recent_k is not None: - items = items[:recent_k] - items.reverse() - return items - - # ------------------------------------------------------------------ - # Azure Durable Function – generate_thread_summary (async) - # ------------------------------------------------------------------ - - async def generate_thread_summary( - self, - user_id: str, - thread_id: str, - recent_k: Optional[int] = None, - poll_interval: float = 2.0, - timeout: float = 120.0, - ) -> dict[str, Any]: - """Trigger the Azure Durable Function to generate a thread summary (async).""" - import asyncio - import json as _json - import aiohttp - - if not self.adf_endpoint: - raise ValueError("adf_endpoint is required to call generate_thread_summary") - - url = f"{self.adf_endpoint.rstrip('/')}/orchestrators/memory_orchestrator" - if self.adf_key: - url += f"?code={self.adf_key}" - - body = { - "user_id": user_id, - "thread_id": thread_id, - "thread_summary_only": True, - } - if recent_k is not None: - body["recent_k"] = recent_k - - async with aiohttp.ClientSession() as session: - async with session.post(url, json=body) as resp: - start_response = await resp.json() - - status_url = start_response.get("statusQueryGetUri") - if not status_url: - return start_response - - deadline = asyncio.get_event_loop().time() + timeout - while asyncio.get_event_loop().time() < deadline: - await asyncio.sleep(poll_interval) - async with session.get(status_url) as resp: - status = await resp.json() - runtime_status = status.get("runtimeStatus", "") - if runtime_status in ("Completed", "Failed", "Terminated"): - return status - - raise TimeoutError( - f"Orchestration did not complete within {timeout}s. " - f"Check status at: {status_url}" - ) - - # ------------------------------------------------------------------ - # Azure Durable Function – extract_facts (async) - # ------------------------------------------------------------------ - - async def extract_facts( - self, - user_id: str, - thread_id: str, - recent_k: Optional[int] = None, - poll_interval: float = 2.0, - timeout: float = 120.0, - ) -> dict[str, Any]: - """Trigger the Azure Durable Function to extract facts from a thread (async).""" - import asyncio - import json as _json - import aiohttp - - if not self.adf_endpoint: - raise ValueError("adf_endpoint is required to call extract_facts") - - url = f"{self.adf_endpoint.rstrip('/')}/orchestrators/memory_orchestrator" - if self.adf_key: - url += f"?code={self.adf_key}" - - body = { - "user_id": user_id, - "thread_id": thread_id, - "extract_facts_only": True, - } - if recent_k is not None: - body["recent_k"] = recent_k - - async with aiohttp.ClientSession() as session: - async with session.post(url, json=body) as resp: - start_response = await resp.json() - - status_url = start_response.get("statusQueryGetUri") - if not status_url: - return start_response - - deadline = asyncio.get_event_loop().time() + timeout - while asyncio.get_event_loop().time() < deadline: - await asyncio.sleep(poll_interval) - async with session.get(status_url) as resp: - status = await resp.json() - runtime_status = status.get("runtimeStatus", "") - if runtime_status in ("Completed", "Failed", "Terminated"): - return status - - raise TimeoutError( - f"Orchestration did not complete within {timeout}s. " - f"Check status at: {status_url}" - ) - - # ------------------------------------------------------------------ - # Azure Durable Function – generate_user_summary (async) - # ------------------------------------------------------------------ - - async def generate_user_summary( - self, - user_id: str, - thread_ids: Optional[list[str]] = None, - recent_k: Optional[int] = None, - poll_interval: float = 2.0, - timeout: float = 120.0, - ) -> dict[str, Any]: - """Trigger the Azure Durable Function to generate a cross-thread user summary (async).""" - import asyncio - import json as _json - import aiohttp - - if not self.adf_endpoint: - raise ValueError("adf_endpoint is required to call generate_user_summary") - - url = f"{self.adf_endpoint.rstrip('/')}/orchestrators/memory_orchestrator" - if self.adf_key: - url += f"?code={self.adf_key}" - - body: dict[str, Any] = { - "user_id": user_id, - "user_summary_only": True, - } - if thread_ids is not None: - body["thread_ids"] = thread_ids - if recent_k is not None: - body["recent_k"] = recent_k - - async with aiohttp.ClientSession() as session: - async with session.post(url, json=body) as resp: - start_response = await resp.json() - - status_url = start_response.get("statusQueryGetUri") - if not status_url: - return start_response - - deadline = asyncio.get_event_loop().time() + timeout - while asyncio.get_event_loop().time() < deadline: - await asyncio.sleep(poll_interval) - async with session.get(status_url) as resp: - status = await resp.json() - runtime_status = status.get("runtimeStatus", "") - if runtime_status in ("Completed", "Failed", "Terminated"): - return status - - raise TimeoutError( - f"Orchestration did not complete within {timeout}s. " - f"Check status at: {status_url}" - ) - - # ------------------------------------------------------------------ - # Cosmos DB – get_user_summary (async) - # ------------------------------------------------------------------ - - async def get_user_summary( - self, - user_id: str, - ) -> list[dict[str, Any]]: - """Retrieve the user summary document(s) for a user from Cosmos DB (async).""" - self._require_cosmos() - - query = ( - "SELECT c.id, c.user_id, c.thread_id, c.role, c.type, " - "c.content, c.metadata, c.created_at " - "FROM c WHERE c.user_id = @user_id AND c.type = 'user_summary' " - "ORDER BY c.created_at DESC" - ) - parameters = [{"name": "@user_id", "value": user_id}] - - results = self._cosmos_container_client.query_items( - query=query, parameters=parameters - ) - return [item async for item in results] - - # ------------------------------------------------------------------ - # Cleanup - # ------------------------------------------------------------------ - - async def close(self) -> None: - """Close the async Cosmos DB client.""" - if self._cosmos_client is not None: - await self._cosmos_client.close() - self._cosmos_client = None - self._cosmos_container_client = None - if self._embeddings_client is not None: - await self._embeddings_client.close() - self._embeddings_client = None - - async def __aenter__(self): - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb): - await self.close() diff --git a/agent_memory_toolkit/cosmos_memory_client.py b/agent_memory_toolkit/cosmos_memory_client.py new file mode 100644 index 0000000..4942290 --- /dev/null +++ b/agent_memory_toolkit/cosmos_memory_client.py @@ -0,0 +1,514 @@ +"""Cosmos DB client layer for the Agent Memory Toolkit. + +Provides :class:`CosmosMemoryStore` — a synchronous client that owns **all** +Cosmos DB interaction logic: connection, container creation, CRUD, and +vector search. + +Embedding generation is **not** this module's responsibility; callers must +supply pre-computed vectors to :meth:`vector_search`. +""" + +from __future__ import annotations + +import logging +from datetime import datetime, timezone +from typing import Any, Optional + +from ._query_builder import _QueryBuilder +from ._utils import ( + _build_memory_query_builder, + _container_policies, + _validate_connection, + _validate_hybrid_search, +) +from .exceptions import ( + CosmosNotConnectedError, + CosmosOperationError, + MemoryNotFoundError, +) +from .models import MemoryRecord + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Sync client +# --------------------------------------------------------------------------- + + +class CosmosMemoryStore: + """Synchronous Cosmos DB client for memory CRUD and vector search. + + Parameters + ---------- + endpoint: + Cosmos DB account endpoint URL. + credential: + Azure ``TokenCredential`` or key string for authentication. + database: + Name of the Cosmos DB database. Defaults to ``"ai_memory"``. + container: + Name of the Cosmos DB container. Defaults to ``"memories"``. + """ + + def __init__( + self, + endpoint: str | None = None, + credential: Any = None, + database: str = "ai_memory", + container: str = "memories", + ) -> None: + self._endpoint = endpoint + self._credential = credential + self._database = database + self._container = container + self._cosmos_client: Any = None + self._container_client: Any = None + + # -- context manager ---------------------------------------------------- + + def __enter__(self) -> CosmosMemoryStore: + return self + + def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: + self.close() + + def close(self) -> None: + """Close the underlying Cosmos client.""" + if self._cosmos_client is not None: + self._cosmos_client.close() + self._cosmos_client = None + self._container_client = None + logger.info("Cosmos client closed") + + # -- connection --------------------------------------------------------- + + def connect(self) -> None: + """Create a :class:`CosmosClient` and obtain the container handle. + + Raises + ------ + ConfigurationError + If any required configuration field is missing. + CosmosOperationError + If the connection to Cosmos DB fails. + """ + _validate_connection( + self._endpoint, self._credential, self._database, self._container + ) + + try: + from azure.cosmos import CosmosClient + + client = CosmosClient( + self._endpoint, credential=self._credential + ) + db = client.get_database_client(self._database) + container = db.get_container_client(self._container) + + self._cosmos_client = client + self._container_client = container + except Exception as exc: + raise CosmosOperationError( + f"Failed to connect to Cosmos DB: {exc}" + ) from exc + + logger.info( + "Connected to Cosmos DB %s/%s", + self._database, + self._container, + ) + + def create_store( + self, + embedding_dimensions: int = 1536, + embedding_data_type: str = "float32", + distance_function: str = "cosine", + full_text_language: str = "en-US", + autoscale_max_ru: int = 1000, + ) -> None: + """Create the database and container, then connect. + + The container is provisioned with: + + * Hierarchical partition key ``[/user_id, /thread_id]`` + * ``quantizedFlat`` vector index on ``/embedding`` + * Full-text index on ``/content`` + * Autoscale throughput (max RU from *autoscale_max_ru*) + + Raises + ------ + ConfigurationError + If required fields are missing. + CosmosOperationError + If the Cosmos DB operation fails. + """ + _validate_connection( + self._endpoint, self._credential, self._database, self._container + ) + + try: + from azure.cosmos import CosmosClient, PartitionKey, ThroughputProperties + + client = CosmosClient( + self._endpoint, credential=self._credential + ) + + db = client.create_database_if_not_exists(id=self._database) + + partition_key = PartitionKey( + path=["/user_id", "/thread_id"], kind="MultiHash" + ) + vec_policy, idx_policy, ft_policy = _container_policies( + embedding_dimensions=embedding_dimensions, + embedding_data_type=embedding_data_type, + distance_function=distance_function, + full_text_language=full_text_language, + ) + + container = db.create_container_if_not_exists( + id=self._container, + partition_key=partition_key, + indexing_policy=idx_policy, + vector_embedding_policy=vec_policy, + full_text_policy=ft_policy, + offer_throughput=ThroughputProperties( + auto_scale_max_throughput=autoscale_max_ru, + ), + ) + self._cosmos_client = client + self._container_client = container + except Exception as exc: + raise CosmosOperationError( + f"Failed to create memory store: {exc}" + ) from exc + + logger.info( + "Created memory store %s/%s", + self._database, + self._container, + ) + + def _require_connected(self) -> None: + """Raise if no active container client.""" + if self._container_client is None: + raise CosmosNotConnectedError() + + # -- upsert ------------------------------------------------------------- + + def upsert(self, record: MemoryRecord) -> None: + """Upsert a single :class:`MemoryRecord` into Cosmos DB.""" + self._require_connected() + body = record.to_cosmos_dict() + try: + self._container_client.upsert_item(body=body) + except Exception as exc: + raise CosmosOperationError( + f"Upsert failed for record {record.id}: {exc}" + ) from exc + logger.info("Upserted record %s", record.id) + + def upsert_batch(self, records: list[MemoryRecord]) -> None: + """Upsert multiple records sequentially.""" + self._require_connected() + for record in records: + self.upsert(record) + logger.info("Upserted batch of %d records", len(records)) + + # -- queries ------------------------------------------------------------ + + def get_memories( + self, + memory_id: Optional[str] = None, + user_id: Optional[str] = None, + thread_id: Optional[str] = None, + role: Optional[str] = None, + memory_type: Optional[str] = None, + recent_k: Optional[int] = None, + ) -> list[dict[str, Any]]: + """Query memories with optional filters. + + Returns raw dicts (callers convert to :class:`MemoryRecord` if needed). + When *recent_k* is given the newest *k* documents are returned in + chronological (oldest-first) order. + """ + self._require_connected() + + qb = _build_memory_query_builder( + memory_id=memory_id, + user_id=user_id, + thread_id=thread_id, + role=role, + memory_type=memory_type, + ) + where = qb.build_where() + parameters = qb.get_parameters() + + if recent_k is not None: + parameters.append({"name": "@recent_k", "value": recent_k}) + query = f"SELECT TOP @recent_k * FROM c{where} ORDER BY c._ts DESC" + else: + query = f"SELECT * FROM c{where}" + + logger.debug("get_memories query: %s", query) + + try: + items = list( + self._container_client.query_items( + query=query, + parameters=parameters or None, + enable_cross_partition_query=True, + ) + ) + except Exception as exc: + raise CosmosOperationError( + f"get_memories query failed: {exc}" + ) from exc + + if recent_k is not None: + items.reverse() + return items + + def get_thread( + self, + thread_id: str, + user_id: Optional[str] = None, + memory_type: Optional[str] = None, + recent_k: Optional[int] = None, + ) -> list[dict[str, Any]]: + """Retrieve an entire thread, oldest-first.""" + self._require_connected() + + qb = _QueryBuilder() + qb.add_filter("c.thread_id", "@thread_id", thread_id) + qb.add_filter("c.user_id", "@user_id", user_id) + qb.add_filter("c.type", "@memory_type", memory_type) + + where = qb.build_where() + parameters = qb.get_parameters() + + query = f"SELECT * FROM c{where} ORDER BY c.created_at DESC" + logger.debug("get_thread query: %s", query) + + try: + items = list( + self._container_client.query_items( + query=query, + parameters=parameters, + enable_cross_partition_query=True, + ) + ) + except Exception as exc: + raise CosmosOperationError( + f"get_thread query failed: {exc}" + ) from exc + + if recent_k is not None: + items = items[:recent_k] + items.reverse() + return items + + def get_user_summary(self, user_id: str) -> list[dict[str, Any]]: + """Retrieve user-summary documents, newest-first.""" + self._require_connected() + + query = ( + "SELECT c.id, c.user_id, c.thread_id, c.role, c.type, " + "c.content, c.metadata, c.created_at " + "FROM c WHERE c.user_id = @user_id AND c.type = 'user_summary' " + "ORDER BY c.created_at DESC" + ) + parameters = [{"name": "@user_id", "value": user_id}] + logger.debug("get_user_summary query: %s", query) + + try: + return list( + self._container_client.query_items( + query=query, + parameters=parameters, + enable_cross_partition_query=True, + ) + ) + except Exception as exc: + raise CosmosOperationError( + f"get_user_summary query failed: {exc}" + ) from exc + + # -- update / delete ---------------------------------------------------- + + def update( + self, + memory_id: str, + content: Optional[str] = None, + role: Optional[str] = None, + memory_type: Optional[str] = None, + metadata: Optional[dict[str, Any]] = None, + ) -> None: + """Update fields on an existing memory document. + + Raises + ------ + MemoryNotFoundError + If no document with *memory_id* exists. + CosmosOperationError + If the underlying Cosmos DB operation fails. + """ + self._require_connected() + + try: + results = list( + self._container_client.query_items( + query="SELECT * FROM c WHERE c.id = @id", + parameters=[{"name": "@id", "value": memory_id}], + enable_cross_partition_query=True, + ) + ) + except Exception as exc: + raise CosmosOperationError( + f"update query failed: {exc}" + ) from exc + + if not results: + raise MemoryNotFoundError(memory_id=memory_id) + + doc = results[0] + if content is not None: + doc["content"] = content + if role is not None: + doc["role"] = role + if memory_type is not None: + doc["type"] = memory_type + if metadata is not None: + doc["metadata"] = metadata + doc["updated_at"] = datetime.now(timezone.utc).isoformat() + + try: + self._container_client.replace_item(item=doc["id"], body=doc) + except Exception as exc: + raise CosmosOperationError( + f"update replace failed for {memory_id}: {exc}" + ) from exc + + logger.info("Updated record %s", memory_id) + + def delete(self, memory_id: str, user_id: str, thread_id: str) -> None: + """Delete a memory document. + + Raises + ------ + MemoryNotFoundError + If no matching document is found. + CosmosOperationError + If the underlying Cosmos DB operation fails. + """ + self._require_connected() + + try: + results = list( + self._container_client.query_items( + query=( + "SELECT TOP 1 c.id FROM c WHERE c.id = @id " + "AND c.thread_id = @thread_id AND c.user_id = @user_id" + ), + parameters=[ + {"name": "@id", "value": memory_id}, + {"name": "@thread_id", "value": thread_id}, + {"name": "@user_id", "value": user_id}, + ], + enable_cross_partition_query=True, + ) + ) + except Exception as exc: + raise CosmosOperationError( + f"delete lookup failed: {exc}" + ) from exc + + if not results: + raise MemoryNotFoundError( + memory_id=memory_id, user_id=user_id, thread_id=thread_id + ) + + try: + self._container_client.delete_item( + item=memory_id, partition_key=[user_id, thread_id] + ) + except Exception as exc: + raise CosmosOperationError( + f"delete failed for {memory_id}: {exc}" + ) from exc + + logger.info("Deleted record %s", memory_id) + + # -- vector search ------------------------------------------------------ + + def vector_search( + self, + query_vector: list[float], + user_id: Optional[str] = None, + role: Optional[str] = None, + memory_type: Optional[str] = None, + thread_id: Optional[str] = None, + hybrid_search: bool = False, + search_terms: Optional[str] = None, + top_k: int = 5, + ) -> list[dict[str, Any]]: + """Run a vector (or hybrid) similarity search. + + Parameters + ---------- + query_vector : list[float] + Pre-computed embedding vector. + search_terms : str, optional + Raw text for the full-text component of a hybrid search. + Required when *hybrid_search* is ``True``. + """ + self._require_connected() + _validate_hybrid_search(hybrid_search, search_terms) + + qb = _build_memory_query_builder( + user_id=user_id, role=role, memory_type=memory_type, thread_id=thread_id + ) + where = qb.build_where() + parameters = qb.get_parameters() + + order_by = "ORDER BY VectorDistance(c.embedding, @embedding)" + if hybrid_search: + order_by = ( + "ORDER BY RANK RRF(" + "VectorDistance(c.embedding, @embedding), " + "FullTextScore(c.content, @key_terms)" + ")" + ) + + query = ( + f"SELECT TOP @top_k c.id, c.user_id, c.role, c.type, c.content, " + f"c.metadata, c.created_at " + f"FROM c{where} " + f"{order_by}" + ) + + parameters.extend( + [ + {"name": "@top_k", "value": top_k}, + {"name": "@embedding", "value": query_vector}, + ] + ) + if hybrid_search: + parameters.append({"name": "@key_terms", "value": search_terms or ""}) + + logger.debug("vector_search query: %s", query) + + try: + items = list( + self._container_client.query_items( + query=query, + parameters=parameters, + enable_cross_partition_query=True, + ) + ) + except Exception as exc: + raise CosmosOperationError( + f"vector_search failed: {exc}" + ) from exc + + return items diff --git a/agent_memory_toolkit/embeddings.py b/agent_memory_toolkit/embeddings.py new file mode 100644 index 0000000..c912439 --- /dev/null +++ b/agent_memory_toolkit/embeddings.py @@ -0,0 +1,159 @@ +"""Synchronous embedding client for the Agent Memory Toolkit. + +Provides :class:`EmbeddingsClient` that lazily initialises an Azure OpenAI +connection and generates embeddings via the OpenAI API. +""" + +from __future__ import annotations + +import logging +from typing import Any + +from .exceptions import ConfigurationError, EmbeddingError + +logger = logging.getLogger(__name__) + +_TOKEN_SCOPE = "https://cognitiveservices.azure.com/.default" + + +# --------------------------------------------------------------------------- +# Sync client +# --------------------------------------------------------------------------- + + +class EmbeddingsClient: + """Synchronous embedding client backed by Azure OpenAI. + + Parameters + ---------- + endpoint: + Azure OpenAI resource endpoint URL. + credential: + Optional Azure ``TokenCredential``. Used when *api_key* is not set + to obtain bearer tokens for the OpenAI service. + api_key: + Optional API key for the Azure OpenAI resource. + model: + Deployment / model name. Defaults to ``"text-embedding-3-large"``. + dimensions: + Optional embedding dimensions override. + api_version: + Azure OpenAI API version. Defaults to ``"2024-12-01-preview"``. + """ + + def __init__( + self, + endpoint: str | None = None, + credential: Any = None, + api_key: str | None = None, + model: str = "text-embedding-3-large", + dimensions: int | None = None, + api_version: str = "2024-12-01-preview", + ) -> None: + self._endpoint = endpoint + self._credential = credential + self._api_key = api_key + self._model = model + self._dimensions = dimensions + self._api_version = api_version + self._client: Any = None # openai.AzureOpenAI (lazy) + + # -- internal helpers --------------------------------------------------- + + def _ensure_client(self) -> Any: + """Lazily create the ``AzureOpenAI`` client on first use.""" + if self._client is not None: + return self._client + + if not self._endpoint: + raise ConfigurationError( + "An embedding endpoint is required", parameter="endpoint" + ) + + from openai import AzureOpenAI + + if self._api_key: + self._client = AzureOpenAI( + api_version=self._api_version, + azure_endpoint=self._endpoint, + api_key=self._api_key, + ) + else: + if self._credential is None: + raise ConfigurationError( + "Either api_key or a TokenCredential is required for embeddings", + parameter="credential", + ) + from azure.identity import get_bearer_token_provider + + token_provider = get_bearer_token_provider(self._credential, _TOKEN_SCOPE) + self._client = AzureOpenAI( + api_version=self._api_version, + azure_endpoint=self._endpoint, + azure_ad_token_provider=token_provider, + ) + + return self._client + + def _build_kwargs(self, input_: str | list[str]) -> dict[str, Any]: + texts = [input_] if isinstance(input_, str) else input_ + logger.debug( + "Embedding request: model=%s, dimensions=%s, texts=%d", + self._model, + self._dimensions, + len(texts), + ) + kwargs: dict[str, Any] = {"input": texts, "model": self._model} + if self._dimensions: + kwargs["dimensions"] = self._dimensions + return kwargs + + # -- public API --------------------------------------------------------- + + def generate(self, text: str) -> list[float]: + """Generate an embedding vector for *text*. + + Raises + ------ + ConfigurationError + If the endpoint or credentials are missing. + EmbeddingError + If the OpenAI API call fails. + """ + client = self._ensure_client() + kwargs = self._build_kwargs(text) + try: + response = client.embeddings.create(**kwargs) + except Exception as exc: + raise EmbeddingError(f"Embedding generation failed: {exc}") from exc + return response.data[0].embedding + + def generate_batch(self, texts: list[str]) -> list[list[float]]: + """Generate embeddings for multiple texts in a single API call. + + Returns a list of embedding vectors **in the same order** as *texts*. + + Raises + ------ + ConfigurationError + If the endpoint or credentials are missing. + EmbeddingError + If the OpenAI API call fails. + """ + if not texts: + return [] + + logger.info("Generating embeddings for batch of %d texts", len(texts)) + client = self._ensure_client() + kwargs = self._build_kwargs(texts) + try: + response = client.embeddings.create(**kwargs) + except Exception as exc: + raise EmbeddingError( + f"Batch embedding generation failed: {exc}" + ) from exc + + # The API returns results with an ``index`` field; sort to guarantee + # the caller receives embeddings in the same order as the input. + sorted_data = sorted(response.data, key=lambda d: d.index) + return [item.embedding for item in sorted_data] diff --git a/agent_memory_toolkit/exceptions.py b/agent_memory_toolkit/exceptions.py new file mode 100644 index 0000000..0434782 --- /dev/null +++ b/agent_memory_toolkit/exceptions.py @@ -0,0 +1,129 @@ +"""Custom exception hierarchy for the Agent Memory Toolkit. + +All exceptions inherit from :class:`AgentMemoryError` so callers can catch +a single base class or handle specific failure modes individually. +""" + + +class AgentMemoryError(Exception): + """Base exception for all Agent Memory Toolkit errors.""" + + +class ConfigurationError(AgentMemoryError): + """Raised when required configuration is missing or invalid. + + Examples: missing cosmos_endpoint, missing ai_foundry_endpoint, + missing credentials. + + Attributes: + parameter: The name of the missing or invalid configuration parameter. + """ + + def __init__(self, message: str | None = None, *, parameter: str | None = None): + self.parameter = parameter + if message is None and parameter: + message = f"Missing or invalid configuration: {parameter}" + super().__init__(message or "Missing or invalid configuration") + + +class ValidationError(AgentMemoryError): + """Raised when input validation fails. + + Examples: invalid role, invalid memory_type, empty user_id. + """ + + +class CosmosNotConnectedError(AgentMemoryError): + """Raised when a Cosmos DB operation is attempted without an active connection.""" + + def __init__(self, message: str | None = None): + super().__init__( + message or "Cosmos DB is not connected. Call connect_cosmos() first." + ) + + +class CosmosOperationError(AgentMemoryError): + """Raised when a Cosmos DB operation fails. + + Covers connection issues, query failures, and other Cosmos DB errors. + """ + + +class MemoryNotFoundError(AgentMemoryError): + """Raised when a memory document is not found. + + Attributes: + memory_id: The ID of the memory that was not found. + user_id: The user ID used in the lookup. + thread_id: The thread ID used in the lookup. + """ + + def __init__( + self, + message: str | None = None, + *, + memory_id: str | None = None, + user_id: str | None = None, + thread_id: str | None = None, + ): + self.memory_id = memory_id + self.user_id = user_id + self.thread_id = thread_id + if message is None: + message = self._build_message() + super().__init__(message) + + def _build_message(self) -> str: + parts: list[str] = [] + if self.memory_id: + parts.append(f"memory_id={self.memory_id!r}") + if self.user_id: + parts.append(f"user_id={self.user_id!r}") + if self.thread_id: + parts.append(f"thread_id={self.thread_id!r}") + detail = ", ".join(parts) + if detail: + return f"Memory not found ({detail})" + return "Memory not found" + + +class EmbeddingError(AgentMemoryError): + """Raised when embedding generation fails.""" + + +class ProcessingError(AgentMemoryError): + """Raised when the processing pipeline (Azure Durable Functions) returns an error.""" + + +class OrchestrationTimeoutError(AgentMemoryError): + """Raised when polling for an orchestration result exceeds the timeout. + + Attributes: + timeout: The timeout value in seconds that was exceeded. + status_url: The URL to check the orchestration status. + """ + + def __init__( + self, + message: str | None = None, + *, + timeout: float | None = None, + status_url: str | None = None, + ): + self.timeout = timeout + self.status_url = status_url + if message is None: + message = self._build_message() + super().__init__(message) + + def _build_message(self) -> str: + msg = "Orchestration timed out" + if self.timeout is not None: + msg = f"Orchestration did not complete within {self.timeout}s" + if self.status_url: + msg += f". Check status at: {self.status_url}" + return msg + + +class AuthenticationError(AgentMemoryError): + """Raised when authentication to Azure services fails.""" diff --git a/agent_memory_toolkit/memory.py b/agent_memory_toolkit/memory.py index d6719b7..08fbfba 100644 --- a/agent_memory_toolkit/memory.py +++ b/agent_memory_toolkit/memory.py @@ -1,100 +1,68 @@ -"""AgentMemory: A class for managing agent memories locally and (eventually) in Cosmos DB.""" +"""AgentMemory: local and cloud agent memory management. -import uuid, os +Thin orchestrator that composes :class:`CosmosMemoryStore`, +:class:`EmbeddingsClient`, and :class:`ProcessingClient` for Cosmos DB +CRUD, vector search, and Azure Durable Functions processing. +""" + +import logging +import os from datetime import datetime, timezone from typing import Any, Optional +from .cosmos_memory_client import CosmosMemoryStore +from .embeddings import EmbeddingsClient +from .exceptions import CosmosNotConnectedError, MemoryNotFoundError, ValidationError +from .models import MemoryRecord +from .processing import ProcessingClient +from ._utils import VALID_ROLES, VALID_TYPES, _make_memory, _resolve_embedding_dimensions -VALID_ROLES = {"agent", "user", "tool", "system"} -VALID_TYPES = {"turn", "summary", "fact", "user_summary"} - - -def _make_memory( - user_id: str, - role: str, - content: str, - memory_type: str = "turn", - agent_id: Optional[str] = None, - metadata: Optional[dict[str, Any]] = None, - memory_id: Optional[str] = None, - thread_id: Optional[str] = None, -) -> dict[str, Any]: - """Create a validated memory dict.""" - if role not in VALID_ROLES: - raise ValueError(f"role must be one of {VALID_ROLES}, got '{role}'") - if memory_type not in VALID_TYPES: - raise ValueError(f"type must be one of {VALID_TYPES}, got '{memory_type}'") - - memory = { - "id": memory_id or str(uuid.uuid4()), - "user_id": user_id, - "thread_id": thread_id or str(uuid.uuid4()), - "role": role, - "type": memory_type, - "content": content, - "metadata": metadata or {}, - "created_at": datetime.now(timezone.utc).isoformat(), - } - - if agent_id is not None: - memory["agent_id"] = agent_id - - return memory +logger = logging.getLogger(__name__) class AgentMemory: - """Manages agent memories with local storage and (future) Cosmos DB support. + """Manages agent memories with local storage and Cosmos DB. Authentication uses ``azure-identity`` by default. If no explicit credential is passed for Cosmos DB or AI Foundry, a - ``DefaultAzureCredential`` is created automatically. This supports - Entra ID (Azure AD) interactive login, managed identity, service - principal, Azure CLI, and other credential flows out of the box. + ``DefaultAzureCredential`` is created automatically. Parameters ---------- cosmos_endpoint : str, optional The Cosmos DB account endpoint URL. cosmos_credential : TokenCredential, optional - An Azure credential (e.g. ``DefaultAzureCredential``, - ``ManagedIdentityCredential``). Falls back to - ``DefaultAzureCredential`` when not provided. + Azure credential for Cosmos DB. cosmos_database : str, optional - The Cosmos DB database name. + Cosmos DB database name. cosmos_container : str, optional - The Cosmos DB container name. + Cosmos DB container name. ai_foundry_endpoint : str, optional - The Azure OpenAI endpoint URL for generating embeddings - (e.g. ``https://myaccount.openai.azure.com/``). + Azure OpenAI endpoint URL for embeddings. ai_foundry_credential : TokenCredential, optional - An Azure credential for the AI Foundry endpoint. Falls back to - ``DefaultAzureCredential`` when not provided. Used to obtain - an Entra ID token for the OpenAI service. + Azure credential for the AI Foundry endpoint. ai_foundry_api_key : str, optional - An API key for Azure OpenAI. When provided this takes - precedence over *ai_foundry_credential*. + API key for Azure OpenAI (takes precedence over credential). embedding_model : str, optional - The embedding model deployment name (default ``text-embedding-3-large``). + Embedding model deployment name (default ``text-embedding-3-large``). + embedding_dimensions : int, optional + Dimensionality of embedding vectors. adf_endpoint : str, optional - Base URL for the Azure Durable Functions API - (e.g. ``http://localhost:7071/api``). + Base URL for the Azure Durable Functions API. adf_key : str, optional Function-level key for authenticating to the Azure Function. - Leave empty when running locally without auth. use_default_credential : bool, optional - When ``True`` (default), automatically creates a - ``DefaultAzureCredential`` for any credential parameter that is - not explicitly supplied. Set to ``False`` to skip this. + Automatically create ``DefaultAzureCredential`` when ``True``. """ def __init__( self, cosmos_endpoint: Optional[str] = None, - cosmos_credential: Optional["TokenCredential"] = None, + cosmos_credential: Optional[Any] = None, cosmos_database: Optional[str] = None, cosmos_container: Optional[str] = None, ai_foundry_endpoint: Optional[str] = None, - ai_foundry_credential: Optional["TokenCredential"] = None, + ai_foundry_credential: Optional[Any] = None, ai_foundry_api_key: Optional[str] = None, embedding_model: str = "text-embedding-3-large", embedding_dimensions: Optional[int] = None, @@ -105,41 +73,51 @@ def __init__( # Local store self.local_memory: list[dict[str, Any]] = [] - # Resolve credentials – fall back to DefaultAzureCredential when - # an explicit credential is not provided and the caller has not - # opted out via use_default_credential=False. - if use_default_credential and (cosmos_credential is None or ai_foundry_credential is None): - try: - from azure.identity import DefaultAzureCredential - _default = DefaultAzureCredential() - except ImportError: - _default = None - - if cosmos_credential is None: - cosmos_credential = _default - if ai_foundry_credential is None: - ai_foundry_credential = _default - - # Cosmos DB configuration - self.cosmos_endpoint = cosmos_endpoint - self.cosmos_credential = cosmos_credential - self.cosmos_database = cosmos_database - self.cosmos_container = cosmos_container - self._cosmos_container_client = None - - # Azure OpenAI embedding configuration - self.ai_foundry_endpoint = ai_foundry_endpoint - self.ai_foundry_credential = ai_foundry_credential - self.ai_foundry_api_key = ai_foundry_api_key - self.embedding_model = embedding_model - self.embedding_dimensions = embedding_dimensions or int( - os.environ.get("EMBEDDING_DIMENSIONS", "0") or "0" - ) or None - self._embeddings_client = None + # Store kwargs directly + self._cosmos_endpoint = cosmos_endpoint + self._cosmos_credential = cosmos_credential + self._cosmos_database = cosmos_database or "ai_memory" + self._cosmos_container = cosmos_container or "memories" + + self._ai_foundry_endpoint = ai_foundry_endpoint + self._ai_foundry_credential = ai_foundry_credential + self._ai_foundry_api_key = ai_foundry_api_key + self._embedding_model = embedding_model + self._embedding_dimensions = _resolve_embedding_dimensions(embedding_dimensions) + + self._adf_endpoint = adf_endpoint + self._adf_key = adf_key + + # Resolve credentials via DefaultAzureCredential when needed + if use_default_credential: + needs_cosmos = self._cosmos_credential is None + needs_embed = self._ai_foundry_credential is None + if needs_cosmos or needs_embed: + try: + from azure.identity import DefaultAzureCredential + _default = DefaultAzureCredential() + except ImportError: + _default = None + if needs_cosmos: + self._cosmos_credential = _default + if needs_embed: + self._ai_foundry_credential = _default + + # Sub-clients (cosmos store created on connect) + self._cosmos_store: Optional[CosmosMemoryStore] = None + self._embeddings_client = EmbeddingsClient( + endpoint=self._ai_foundry_endpoint, + credential=self._ai_foundry_credential, + api_key=self._ai_foundry_api_key, + model=self._embedding_model, + dimensions=self._embedding_dimensions, + ) + self._processing_client = ProcessingClient( + endpoint=self._adf_endpoint, + key=self._adf_key, + ) - # Azure Durable Functions configuration - self.adf_endpoint = adf_endpoint - self.adf_key = adf_key + logger.info("AgentMemory initialized") # ------------------------------------------------------------------ # Local operations @@ -166,6 +144,7 @@ def add_local( thread_id=thread_id, ) self.local_memory.append(memory) + logger.debug("add_local id=%s role=%s type=%s", memory["id"], role, memory_type) def get_local( self, @@ -179,6 +158,10 @@ def get_local( All filter parameters are optional. When none are provided every memory is returned. Filters are combined with AND logic. """ + logger.debug( + "get_local memory_id=%s user_id=%s role=%s type=%s", + memory_id, user_id, role, memory_type, + ) results = self.local_memory if memory_id is not None: @@ -204,7 +187,12 @@ def update_local( Only the fields that are provided (not ``None``) will be updated. - Raises ``KeyError`` if no memory with the given id exists. + Raises + ------ + MemoryNotFoundError + If no memory with the given id exists. + ValidationError + If an invalid role or memory_type is provided. """ for memory in self.local_memory: if memory["id"] == memory_id: @@ -212,30 +200,33 @@ def update_local( memory["content"] = content if role is not None: if role not in VALID_ROLES: - raise ValueError(f"role must be one of {VALID_ROLES}, got '{role}'") + raise ValidationError(f"role must be one of {VALID_ROLES}, got '{role}'") memory["role"] = role if memory_type is not None: if memory_type not in VALID_TYPES: - raise ValueError(f"type must be one of {VALID_TYPES}, got '{memory_type}'") + raise ValidationError(f"type must be one of {VALID_TYPES}, got '{memory_type}'") memory["type"] = memory_type if metadata is not None: memory["metadata"] = metadata memory["updated_at"] = datetime.now(timezone.utc).isoformat() return - raise KeyError(f"No memory found with id '{memory_id}'") + raise MemoryNotFoundError(memory_id=memory_id) def delete_local(self, memory_id: str) -> None: """Delete a memory from the local store by id. - Raises ``KeyError`` if no memory with the given id exists. + Raises + ------ + MemoryNotFoundError + If no memory with the given id exists. """ for i, memory in enumerate(self.local_memory): if memory["id"] == memory_id: self.local_memory.pop(i) return - raise KeyError(f"No memory found with id '{memory_id}'") + raise MemoryNotFoundError(memory_id=memory_id) # ------------------------------------------------------------------ # Cosmos DB connection @@ -244,7 +235,7 @@ def delete_local(self, memory_id: str) -> None: def connect_cosmos( self, endpoint: Optional[str] = None, - credential: Optional["TokenCredential"] = None, + credential: Optional[Any] = None, database: Optional[str] = None, container: Optional[str] = None, ) -> None: @@ -252,35 +243,26 @@ def connect_cosmos( Parameters override whatever was set in ``__init__``. After this call the Cosmos CRUD methods are ready to use. - - Raises ``ValueError`` if required connection details are missing. """ - from azure.cosmos import CosmosClient - - self.cosmos_endpoint = endpoint or self.cosmos_endpoint - self.cosmos_credential = credential or self.cosmos_credential - self.cosmos_database = database or self.cosmos_database - self.cosmos_container = container or self.cosmos_container - - if not self.cosmos_endpoint: - raise ValueError("cosmos_endpoint is required") - if not self.cosmos_credential: - raise ValueError("cosmos_credential is required") - if not self.cosmos_database: - raise ValueError("cosmos_database is required") - if not self.cosmos_container: - raise ValueError("cosmos_container is required") - - client = CosmosClient(self.cosmos_endpoint, credential=self.cosmos_credential) - db = client.get_database_client(self.cosmos_database) - self._cosmos_container_client = db.get_container_client(self.cosmos_container) + self._cosmos_endpoint = endpoint or self._cosmos_endpoint + self._cosmos_credential = credential or self._cosmos_credential + self._cosmos_database = database or self._cosmos_database + self._cosmos_container = container or self._cosmos_container + + self._cosmos_store = CosmosMemoryStore( + endpoint=self._cosmos_endpoint, + credential=self._cosmos_credential, + database=self._cosmos_database, + container=self._cosmos_container, + ) + self._cosmos_store.connect() def create_memory_store( self, database: Optional[str] = None, container: Optional[str] = None, endpoint: Optional[str] = None, - credential: Optional["TokenCredential"] = None, + credential: Optional[Any] = None, embedding_dimensions: Optional[int] = None, embedding_data_type: Optional[str] = None, distance_function: Optional[str] = None, @@ -288,187 +270,32 @@ def create_memory_store( ) -> None: """Create the Cosmos DB database and container for memories. - Skips creation if the database or container already exists. After successful creation the instance is connected and ready for CRUD operations. - - The container is configured with: - - * **Hierarchical partition key** – ``/user_id`` then ``/thread_id`` - * **Vector embedding policy** – ``quantizedFlat`` index on the - ``/embedding`` path - * **Full-text index & policy** – English analyzer on ``/content`` - * **Autoscale throughput** – max ``1000`` RU/s - - Parameters - ---------- - database : str, optional - Database name. Falls back to ``self.cosmos_database``. - container : str, optional - Container name. Falls back to ``self.cosmos_container``. - endpoint : str, optional - Cosmos DB endpoint. Falls back to ``self.cosmos_endpoint``. - credential : TokenCredential, optional - Azure credential. Falls back to ``self.cosmos_credential``. - embedding_dimensions : int, optional - Dimensionality of the embedding vectors. Falls back to - env var ``EMBEDDING_DIMENSIONS``, then ``3072``. - embedding_data_type : str, optional - Data type for the vector (e.g. ``float32``, ``int8``). - Falls back to env var ``EMBEDDING_DATA_TYPE``, then - ``float32``. - distance_function : str, optional - Distance function (e.g. ``cosine``, ``euclidean``, - ``dotproduct``). Falls back to env var - ``EMBEDDING_DISTANCE_FUNCTION``, then ``cosine``. - full_text_language : str, optional - Language for the full-text index on ``/content`` - (e.g. ``en-US``, ``fr-FR``). Falls back to env var - ``FULL_TEXT_LANGUAGE``, then ``en-US``. """ - from azure.cosmos import CosmosClient, PartitionKey, ThroughputProperties - - self.cosmos_endpoint = endpoint or self.cosmos_endpoint - self.cosmos_credential = credential or self.cosmos_credential - self.cosmos_database = database or self.cosmos_database - self.cosmos_container = container or self.cosmos_container - - if not self.cosmos_endpoint: - raise ValueError("cosmos_endpoint is required") - if not self.cosmos_credential: - raise ValueError("cosmos_credential is required") - if not self.cosmos_database: - raise ValueError("cosmos_database is required") - if not self.cosmos_container: - raise ValueError("cosmos_container is required") - - import os as _os - - embedding_dimensions = embedding_dimensions or int( - _os.environ.get("EMBEDDING_DIMENSIONS", "1536") - ) - embedding_data_type = ( - embedding_data_type - or _os.environ.get("EMBEDDING_DATA_TYPE", "float32") - ) - distance_function = ( - distance_function - or _os.environ.get("EMBEDDING_DISTANCE_FUNCTION", "cosine") - ) - full_text_language = ( - full_text_language - or _os.environ.get("FULL_TEXT_LANGUAGE", "en-US") + self._cosmos_endpoint = endpoint or self._cosmos_endpoint + self._cosmos_credential = credential or self._cosmos_credential + self._cosmos_database = database or self._cosmos_database + self._cosmos_container = container or self._cosmos_container + + self._cosmos_store = CosmosMemoryStore( + endpoint=self._cosmos_endpoint, + credential=self._cosmos_credential, + database=self._cosmos_database, + container=self._cosmos_container, ) - autoscale_max_ru = int( - _os.environ.get("COSMOS_DB_AUTOSCALE_MAX_RU", "1000") + self._cosmos_store.create_store( + embedding_dimensions=embedding_dimensions or self._embedding_dimensions or 1536, + embedding_data_type=embedding_data_type or "float32", + distance_function=distance_function or "cosine", + full_text_language=full_text_language or "en-US", ) - client = CosmosClient(self.cosmos_endpoint, credential=self.cosmos_credential) - - # ---- Database (create if not exists) ---- - db = client.create_database_if_not_exists(id=self.cosmos_database) - - # ---- Container (create if not exists) ---- - partition_key = PartitionKey( - path=["/user_id", "/thread_id"], - kind="MultiHash", - ) - - vector_embedding_policy = { - "vectorEmbeddings": [ - { - "path": "/embedding", - "dataType": embedding_data_type, - "distanceFunction": distance_function, - "dimensions": embedding_dimensions, - } - ] - } - - indexing_policy = { - "includedPaths": [{"path": "/*"}], - "excludedPaths": [{"path": "/embedding/*"}], - "vectorIndexes": [ - { - "path": "/embedding", - "type": "quantizedFlat", - } - ], - "fullTextIndexes": [ - {"path": "/content"} - ], - } - - full_text_policy = { - "defaultLanguage": full_text_language, - "fullTextPaths": [ - {"path": "/content", "language": full_text_language} - ], - } - - container_obj = db.create_container_if_not_exists( - id=self.cosmos_container, - partition_key=partition_key, - indexing_policy=indexing_policy, - vector_embedding_policy=vector_embedding_policy, - full_text_policy=full_text_policy, - offer_throughput=ThroughputProperties( - auto_scale_max_throughput=autoscale_max_ru, - ), - ) - - self._cosmos_container_client = container_obj - - def _require_cosmos(self): + def _require_cosmos(self) -> None: """Raise if Cosmos DB is not connected.""" - if self._cosmos_container_client is None: - raise RuntimeError( - "Cosmos DB is not connected. Call connect_cosmos() first." - ) - - # ------------------------------------------------------------------ - # Embeddings helper - # ------------------------------------------------------------------ - - def _get_embedding(self, text: str) -> list[float]: - """Generate a vector embedding for *text* via Azure OpenAI.""" - if self._embeddings_client is None: - from openai import AzureOpenAI - - if not self.ai_foundry_endpoint: - raise ValueError("ai_foundry_endpoint is required for embeddings") - - if self.ai_foundry_api_key: - self._embeddings_client = AzureOpenAI( - api_version="2024-12-01-preview", - azure_endpoint=self.ai_foundry_endpoint, - api_key=self.ai_foundry_api_key, - ) - else: - if not self.ai_foundry_credential: - raise ValueError( - "ai_foundry_credential or ai_foundry_api_key is required for embeddings" - ) - from azure.identity import get_bearer_token_provider - - token_provider = get_bearer_token_provider( - self.ai_foundry_credential, - "https://cognitiveservices.azure.com/.default", - ) - self._embeddings_client = AzureOpenAI( - api_version="2024-12-01-preview", - azure_endpoint=self.ai_foundry_endpoint, - azure_ad_token_provider=token_provider, - ) - - kwargs: dict[str, Any] = { - "input": [text], - "model": self.embedding_model, - } - if self.embedding_dimensions: - kwargs["dimensions"] = self.embedding_dimensions - response = self._embeddings_client.embeddings.create(**kwargs) - return response.data[0].embedding + if self._cosmos_store is None: + raise CosmosNotConnectedError() + self._cosmos_store._require_connected() # ------------------------------------------------------------------ # Cosmos DB operations @@ -485,15 +312,18 @@ def add_cosmos( ) -> None: """Add a memory to Cosmos DB.""" self._require_cosmos() - memory = _make_memory( - user_id=user_id, - role=role, - content=content, - memory_type=memory_type, - metadata=metadata, - thread_id=thread_id, - ) - self._cosmos_container_client.upsert_item(body=memory) + kwargs: dict[str, Any] = { + "user_id": user_id, + "role": role, + "content": content, + "memory_type": memory_type, + "metadata": metadata or {}, + } + if thread_id is not None: + kwargs["thread_id"] = thread_id + record = MemoryRecord(**kwargs) + self._cosmos_store.upsert(record) + logger.info("add_cosmos id=%s role=%s type=%s", record.id, role, memory_type) def push_to_cosmos(self) -> None: """Insert all local memories into Cosmos DB. @@ -502,9 +332,9 @@ def push_to_cosmos(self) -> None: ``id``, ``thread_id``, timestamps, and metadata. """ self._require_cosmos() - - for memory in self.local_memory: - self._cosmos_container_client.upsert_item(body=dict(memory)) + logger.info("push_to_cosmos count=%d", len(self.local_memory)) + records = [MemoryRecord.from_cosmos_dict(dict(m)) for m in self.local_memory] + self._cosmos_store.upsert_batch(records) def get_memories( self, @@ -527,43 +357,20 @@ def get_memories( (ordered by ``_ts`` descending, then reversed to chronological). """ self._require_cosmos() - - conditions: list[str] = [] - parameters: list[dict[str, Any]] = [] - - if memory_id is not None: - conditions.append("c.id = @memory_id") - parameters.append({"name": "@memory_id", "value": memory_id}) - if user_id is not None: - conditions.append("c.user_id = @user_id") - parameters.append({"name": "@user_id", "value": user_id}) - if thread_id is not None: - conditions.append("c.thread_id = @thread_id") - parameters.append({"name": "@thread_id", "value": thread_id}) - if role is not None: - conditions.append("c.role = @role") - parameters.append({"name": "@role", "value": role}) - if memory_type is not None: - conditions.append("c.type = @memory_type") - parameters.append({"name": "@memory_type", "value": memory_type}) - - where = (" WHERE " + " AND ".join(conditions)) if conditions else "" - - if recent_k is not None: - top_clause = "TOP @recent_k " - parameters.append({"name": "@recent_k", "value": recent_k}) - query = f"SELECT {top_clause}* FROM c{where} ORDER BY c._ts DESC" - else: - query = f"SELECT * FROM c{where}" - - items = self._cosmos_container_client.query_items( - query=query, - parameters=parameters or None, - enable_cross_partition_query=True + logger.debug( + "get_memories filters: memory_id=%s user_id=%s thread_id=%s role=%s type=%s recent_k=%s", + memory_id, user_id, thread_id, role, memory_type, recent_k, ) - results = list(items) - if recent_k is not None: - results.reverse() + results = self._cosmos_store.get_memories( + memory_id=memory_id, + user_id=user_id, + thread_id=thread_id, + role=role, + memory_type=memory_type, + recent_k=recent_k, + ) + if not results: + logger.warning("get_memories returned empty results") return results def update_cosmos( @@ -574,67 +381,23 @@ def update_cosmos( memory_type: Optional[str] = None, metadata: Optional[dict[str, Any]] = None, ) -> None: - """Update a memory in Cosmos DB. - - Raises ``KeyError`` if no memory with the given id exists. - """ + """Update a memory in Cosmos DB.""" self._require_cosmos() - - # Fetch current document - results = list(self._cosmos_container_client.query_items( - query="SELECT * FROM c WHERE c.id = @id", - parameters=[{"name": "@id", "value": memory_id}], - enable_cross_partition_query=True - - )) - if not results: - raise KeyError(f"No memory found with id '{memory_id}'") - - doc = results[0] - if content is not None: - doc["content"] = content - if role is not None: - if role not in VALID_ROLES: - raise ValueError(f"role must be one of {VALID_ROLES}, got '{role}'") - doc["role"] = role - if memory_type is not None: - if memory_type not in VALID_TYPES: - raise ValueError(f"type must be one of {VALID_TYPES}, got '{memory_type}'") - doc["type"] = memory_type - if metadata is not None: - doc["metadata"] = metadata - doc["updated_at"] = datetime.now(timezone.utc).isoformat() - - self._cosmos_container_client.replace_item(item=doc["id"], body=doc) + self._cosmos_store.update( + memory_id=memory_id, + content=content, + role=role, + memory_type=memory_type, + metadata=metadata, + ) def delete_cosmos(self, memory_id: str, thread_id: str, user_id: str) -> None: - """Delete a memory from Cosmos DB. - - Raises ``KeyError`` if no memory with the given id exists. - """ + """Delete a memory from Cosmos DB.""" self._require_cosmos() - - results = list(self._cosmos_container_client.query_items( - query=( - "SELECT TOP 1 c.id FROM c WHERE c.id = @id " - "AND c.thread_id = @thread_id AND c.user_id = @user_id" - ), - parameters=[ - {"name": "@id", "value": memory_id}, - {"name": "@thread_id", "value": thread_id}, - {"name": "@user_id", "value": user_id}, - ], - enable_cross_partition_query=True - - )) - if not results: - raise KeyError( - f"No memory found with id '{memory_id}' for user_id '{user_id}' and thread_id '{thread_id}'" - ) - - self._cosmos_container_client.delete_item( - item=memory_id, - partition_key=[user_id, thread_id], + self._cosmos_store.delete( + memory_id=memory_id, + user_id=user_id, + thread_id=thread_id, ) def search_cosmos( @@ -648,70 +411,44 @@ def search_cosmos( hybrid_search: bool = False, top_k: int = 5, ) -> list[dict[str, Any]]: - """Search memories in Cosmos DB. + """Search memories in Cosmos DB using vector similarity. - 1. Embeds ``search_terms`` via the configured AI Foundry model. + 1. Embeds *search_terms* via the configured embedding model. 2. Runs a vector similarity query against the Cosmos DB container. - When ``hybrid_search=True``, combines vector and full-text - ranking via RRF in the ``ORDER BY`` clause. - 3. Optionally filters by ``memory_id``, ``user_id``, ``role``, - ``memory_type``, and/or ``thread_id``. - 4. Returns up to ``top_k`` results ordered by similarity. + 3. Optionally filters by the remaining keyword parameters. + 4. Returns up to *top_k* results ordered by similarity. """ self._require_cosmos() - - query_vector = self._get_embedding(search_terms) - - # Build optional WHERE filters - conditions: list[str] = [] - parameters: list[dict[str, Any]] = [] - - if memory_id is not None: - conditions.append("c.id = @memory_id") - parameters.append({"name": "@memory_id", "value": memory_id}) - if user_id is not None: - conditions.append("c.user_id = @user_id") - parameters.append({"name": "@user_id", "value": user_id}) - if role is not None: - conditions.append("c.role = @role") - parameters.append({"name": "@role", "value": role}) - if memory_type is not None: - conditions.append("c.type = @memory_type") - parameters.append({"name": "@memory_type", "value": memory_type}) - if thread_id is not None: - conditions.append("c.thread_id = @thread_id") - parameters.append({"name": "@thread_id", "value": thread_id}) - - where = (" WHERE " + " AND ".join(conditions)) if conditions else "" - - order_by = "ORDER BY VectorDistance(c.embedding, @embedding)" - if hybrid_search: - order_by = ( - "ORDER BY RANK RRF(" - "VectorDistance(c.embedding, @embedding), " - "FullTextScore(c.content, @key_terms)" - ")" - ) - - query = ( - f"SELECT TOP @top_k c.id, c.user_id, c.role, c.type, c.content, " - f"c.metadata, c.created_at " - f"FROM c{where} " - f"{order_by}" + logger.info( + "search_cosmos terms_len=%d top_k=%d hybrid_search=%s", + len(search_terms), + top_k, + hybrid_search, ) - parameters.extend([ - {"name": "@top_k", "value": top_k}, - {"name": "@embedding", "value": query_vector}, - ]) - if hybrid_search: - parameters.append({"name": "@key_terms", "value": search_terms}) - - items = self._cosmos_container_client.query_items( - query=query, - parameters=parameters, - enable_cross_partition_query=True, + logger.debug( + "search_cosmos search_terms=%s", + search_terms[:50] + "..." if len(search_terms) > 50 else search_terms, + ) + query_vector = self._embeddings_client.generate(search_terms) + results = self._cosmos_store.vector_search( + query_vector=query_vector, + user_id=user_id, + role=role, + memory_type=memory_type, + thread_id=thread_id, + hybrid_search=hybrid_search, + search_terms=search_terms, + top_k=top_k, ) - return list(items) + # Post-filter by memory_id (not supported directly by vector_search) + if memory_id is not None: + results = [r for r in results if r.get("id") == memory_id] + if not results: + logger.warning( + "search_cosmos returned empty results (terms_len=%d)", + len(search_terms), + ) + return results def get_thread( self, @@ -722,57 +459,23 @@ def get_thread( ) -> list[dict[str, Any]]: """Retrieve an entire thread from Cosmos DB. - Parameters - ---------- - thread_id : str - The thread to retrieve (required). - user_id : str, optional - If provided, only return memories belonging to this user. - memory_type : str, optional - If provided, only return memories of this type - (e.g. ``"turn"``, ``"summary"``, ``"fact"``). - recent_k : int, optional - If provided, return only the *k* most recent documents - (by ``created_at``). Otherwise all documents are returned. - - Returns - ------- - list[dict] - Memories sorted in chronological order (oldest first). + Returns memories sorted in chronological order (oldest first). """ self._require_cosmos() + return self._cosmos_store.get_thread( + thread_id=thread_id, + user_id=user_id, + memory_type=memory_type, + recent_k=recent_k, + ) - conditions: list[str] = ["c.thread_id = @thread_id"] - parameters: list[dict[str, Any]] = [ - {"name": "@thread_id", "value": thread_id}, - ] - - if user_id is not None: - conditions.append("c.user_id = @user_id") - parameters.append({"name": "@user_id", "value": user_id}) - - if memory_type is not None: - conditions.append("c.type = @memory_type") - parameters.append({"name": "@memory_type", "value": memory_type}) - - where = " WHERE " + " AND ".join(conditions) - query = f"SELECT * FROM c{where} ORDER BY c.created_at DESC" - - items = list(self._cosmos_container_client.query_items( - query=query, - parameters=parameters, - enable_cross_partition_query=True, - )) - - if recent_k is not None: - items = items[:recent_k] - - # Return in chronological order (oldest first) - items.reverse() - return items + def get_user_summary(self, user_id: str) -> list[dict[str, Any]]: + """Retrieve user summary documents from Cosmos DB, newest first.""" + self._require_cosmos() + return self._cosmos_store.get_user_summary(user_id=user_id) # ------------------------------------------------------------------ - # Azure Durable Function – generate_thread_summary + # Processing (Azure Durable Functions) # ------------------------------------------------------------------ def generate_thread_summary( @@ -783,286 +486,56 @@ def generate_thread_summary( poll_interval: float = 2.0, timeout: float = 120.0, ) -> dict[str, Any]: - """Trigger the Azure Durable Function to generate a thread summary. - - Starts the ``memory_orchestrator`` with ``thread_summary_only=True`` - and polls until the orchestration completes or *timeout* seconds - elapse. - - Parameters - ---------- - user_id : str - The user whose memories to summarize. - thread_id : str - The conversation thread to summarize. - recent_k : int, optional - If provided, only the most recent *k* memories are - included in the summary. - poll_interval : float - Seconds between status polls (default 2). - timeout : float - Maximum seconds to wait for completion (default 120). - - Returns - ------- - dict - The orchestration result containing the summary. - """ - import time - import urllib.request - import json as _json - - if not self.adf_endpoint: - raise ValueError("adf_endpoint is required to call generate_thread_summary") - - # Build the starter URL - url = f"{self.adf_endpoint.rstrip('/')}/orchestrators/memory_orchestrator" - if self.adf_key: - url += f"?code={self.adf_key}" - - body = { - "user_id": user_id, - "thread_id": thread_id, - "thread_summary_only": True, - } - if recent_k is not None: - body["recent_k"] = recent_k - - data = _json.dumps(body).encode("utf-8") - req = urllib.request.Request( - url, data=data, - headers={"Content-Type": "application/json"}, - method="POST", + """Trigger the Azure Durable Function to generate a thread summary.""" + logger.info( + "generate_thread_summary started user_id=%s thread_id=%s", + user_id, + thread_id, ) - - with urllib.request.urlopen(req) as resp: - start_response = _json.loads(resp.read().decode("utf-8")) - - status_url = start_response.get("statusQueryGetUri") - if not status_url: - return start_response - - # Poll for completion - deadline = time.monotonic() + timeout - while time.monotonic() < deadline: - time.sleep(poll_interval) - status_req = urllib.request.Request(status_url, method="GET") - with urllib.request.urlopen(status_req) as resp: - status = _json.loads(resp.read().decode("utf-8")) - runtime_status = status.get("runtimeStatus", "") - if runtime_status in ("Completed", "Failed", "Terminated"): - return status - - raise TimeoutError( - f"Orchestration did not complete within {timeout}s. " - f"Check status at: {status_url}" + return self._processing_client.generate_thread_summary( + user_id=user_id, + thread_id=thread_id, + recent_k=recent_k, + poll_interval=poll_interval, + timeout=timeout, ) - # ------------------------------------------------------------------ - # Azure Durable Function – generate_user_summary - # ------------------------------------------------------------------ - - def generate_user_summary( + def extract_facts( self, user_id: str, - thread_ids: Optional[list[str]] = None, + thread_id: str, recent_k: Optional[int] = None, poll_interval: float = 2.0, timeout: float = 120.0, ) -> dict[str, Any]: - """Trigger the Azure Durable Function to generate a cross-thread user summary. - - Aggregates memories across all (or selected) threads for a user - and produces a structured profile covering preferences, account - state, compliance details, and behavioural patterns. - - Parameters - ---------- - user_id : str - The user to summarize across threads. - thread_ids : list[str], optional - If provided, only these threads are included. Otherwise all - threads for the user are used. - recent_k : int, optional - If provided, only the most recent *k* memories per thread - are included. - poll_interval : float - Seconds between status polls (default 2). - timeout : float - Maximum seconds to wait for completion (default 120). - - Returns - ------- - dict - The orchestration result containing the user summary. - """ - import time - import urllib.request - import json as _json - - if not self.adf_endpoint: - raise ValueError("adf_endpoint is required to call generate_user_summary") - - url = f"{self.adf_endpoint.rstrip('/')}/orchestrators/memory_orchestrator" - if self.adf_key: - url += f"?code={self.adf_key}" - - body: dict[str, Any] = { - "user_id": user_id, - "user_summary_only": True, - } - if thread_ids is not None: - body["thread_ids"] = thread_ids - if recent_k is not None: - body["recent_k"] = recent_k - - data = _json.dumps(body).encode("utf-8") - req = urllib.request.Request( - url, data=data, - headers={"Content-Type": "application/json"}, - method="POST", + """Trigger the Azure Durable Function to extract facts from a thread.""" + logger.info( + "extract_facts started user_id=%s thread_id=%s", + user_id, + thread_id, ) - - with urllib.request.urlopen(req) as resp: - start_response = _json.loads(resp.read().decode("utf-8")) - - status_url = start_response.get("statusQueryGetUri") - if not status_url: - return start_response - - deadline = time.monotonic() + timeout - while time.monotonic() < deadline: - time.sleep(poll_interval) - status_req = urllib.request.Request(status_url, method="GET") - with urllib.request.urlopen(status_req) as resp: - status = _json.loads(resp.read().decode("utf-8")) - runtime_status = status.get("runtimeStatus", "") - if runtime_status in ("Completed", "Failed", "Terminated"): - return status - - raise TimeoutError( - f"Orchestration did not complete within {timeout}s. " - f"Check status at: {status_url}" - ) - - # ------------------------------------------------------------------ - # Cosmos DB – get_user_summary - # ------------------------------------------------------------------ - - def get_user_summary( - self, - user_id: str, - ) -> list[dict[str, Any]]: - """Retrieve the user summary document(s) for a user from Cosmos DB. - - Parameters - ---------- - user_id : str - The user whose summary to retrieve. - - Returns - ------- - list[dict] - User summary documents, newest first. - """ - self._require_cosmos() - - query = ( - "SELECT c.id, c.user_id, c.thread_id, c.role, c.type, " - "c.content, c.metadata, c.created_at " - "FROM c WHERE c.user_id = @user_id AND c.type = 'user_summary' " - "ORDER BY c.created_at DESC" + return self._processing_client.extract_facts( + user_id=user_id, + thread_id=thread_id, + recent_k=recent_k, + poll_interval=poll_interval, + timeout=timeout, ) - parameters = [{"name": "@user_id", "value": user_id}] - - return list(self._cosmos_container_client.query_items( - query=query, - parameters=parameters, - enable_cross_partition_query=True, - )) - - # ------------------------------------------------------------------ - # Azure Durable Function – extract_facts - # ------------------------------------------------------------------ - def extract_facts( + def generate_user_summary( self, user_id: str, - thread_id: str, + thread_ids: Optional[list[str]] = None, recent_k: Optional[int] = None, poll_interval: float = 2.0, timeout: float = 120.0, ) -> dict[str, Any]: - """Trigger the Azure Durable Function to extract facts from a thread. - - Starts the ``memory_orchestrator`` with ``extract_facts_only=True`` - and polls until the orchestration completes or *timeout* seconds - elapse. - - Parameters - ---------- - user_id : str - The user whose memories to extract facts from. - thread_id : str - The conversation thread to extract facts from. - recent_k : int, optional - If provided, only the most recent *k* memories are - included in the extraction. - poll_interval : float - Seconds between status polls (default 2). - timeout : float - Maximum seconds to wait for completion (default 120). - - Returns - ------- - dict - The orchestration result containing the extracted facts. - """ - import time - import urllib.request - import json as _json - - if not self.adf_endpoint: - raise ValueError("adf_endpoint is required to call extract_facts") - - url = f"{self.adf_endpoint.rstrip('/')}/orchestrators/memory_orchestrator" - if self.adf_key: - url += f"?code={self.adf_key}" - - body = { - "user_id": user_id, - "thread_id": thread_id, - "extract_facts_only": True, - } - if recent_k is not None: - body["recent_k"] = recent_k - - data = _json.dumps(body).encode("utf-8") - req = urllib.request.Request( - url, data=data, - headers={"Content-Type": "application/json"}, - method="POST", - ) - - with urllib.request.urlopen(req) as resp: - start_response = _json.loads(resp.read().decode("utf-8")) - - status_url = start_response.get("statusQueryGetUri") - if not status_url: - return start_response - - # Poll for completion - deadline = time.monotonic() + timeout - while time.monotonic() < deadline: - time.sleep(poll_interval) - status_req = urllib.request.Request(status_url, method="GET") - with urllib.request.urlopen(status_req) as resp: - status = _json.loads(resp.read().decode("utf-8")) - runtime_status = status.get("runtimeStatus", "") - if runtime_status in ("Completed", "Failed", "Terminated"): - return status - - raise TimeoutError( - f"Orchestration did not complete within {timeout}s. " - f"Check status at: {status_url}" + """Trigger the Azure Durable Function to generate a cross-thread user summary.""" + logger.info("generate_user_summary started user_id=%s", user_id) + return self._processing_client.generate_user_summary( + user_id=user_id, + thread_ids=thread_ids, + recent_k=recent_k, + poll_interval=poll_interval, + timeout=timeout, ) diff --git a/agent_memory_toolkit/models.py b/agent_memory_toolkit/models.py new file mode 100644 index 0000000..3d14ab0 --- /dev/null +++ b/agent_memory_toolkit/models.py @@ -0,0 +1,172 @@ +"""Pydantic data models for the Agent Memory Toolkit. + +Provides typed, validated models that replace raw dicts for memory records, +search results, and orchestration responses. All models serialize to/from +Cosmos DB-compatible JSON. +""" + +import logging +import uuid +from datetime import datetime, timezone +from enum import Enum +from typing import Any, Optional + +from pydantic import BaseModel, Field, field_validator + +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Enums +# --------------------------------------------------------------------------- + + +class MemoryRole(str, Enum): + """Allowed roles for a memory record.""" + + user = "user" + agent = "agent" + tool = "tool" + system = "system" + + +class MemoryType(str, Enum): + """Allowed memory types stored in Cosmos DB.""" + + turn = "turn" + summary = "summary" + fact = "fact" + user_summary = "user_summary" + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _uuid4_str() -> str: + return str(uuid.uuid4()) + + +def _utc_now_iso() -> str: + return datetime.now(timezone.utc).isoformat() + + +# --------------------------------------------------------------------------- +# Core model +# --------------------------------------------------------------------------- + + +class MemoryRecord(BaseModel): + """A single memory document stored in Cosmos DB. + + The ``memory_type`` field is named ``memory_type`` in Python to avoid + shadowing the built-in ``type``, but it serializes to/from ``"type"`` + for Cosmos DB compatibility via a Pydantic alias. + """ + + model_config = { + "populate_by_name": True, + "use_enum_values": True, + } + + id: str = Field(default_factory=_uuid4_str) + user_id: str + thread_id: str = Field(default_factory=_uuid4_str) + role: MemoryRole + memory_type: MemoryType = Field(alias="type", default=MemoryType.turn) + content: str + metadata: dict[str, Any] = Field(default_factory=dict) + embedding: Optional[list[float]] = None + agent_id: Optional[str] = None + created_at: str = Field(default_factory=_utc_now_iso) + updated_at: Optional[str] = None + + # -- validators ---------------------------------------------------------- + + @field_validator("role", mode="before") + @classmethod + def _validate_role(cls, v: Any) -> Any: + if isinstance(v, str): + try: + return MemoryRole(v) + except ValueError: + valid = ", ".join(r.value for r in MemoryRole) + raise ValueError( + f"role must be one of {{{valid}}}, got '{v}'" + ) + return v + + @field_validator("memory_type", mode="before") + @classmethod + def _validate_memory_type(cls, v: Any) -> Any: + if isinstance(v, str): + try: + return MemoryType(v) + except ValueError: + valid = ", ".join(t.value for t in MemoryType) + raise ValueError( + f"type must be one of {{{valid}}}, got '{v}'" + ) + return v + + # -- serialization helpers ----------------------------------------------- + + def to_cosmos_dict(self) -> dict[str, Any]: + """Return a dict suitable for Cosmos DB upsert. + + * Uses ``"type"`` as the key name (not ``"memory_type"``). + * Omits keys whose value is ``None``. + """ + data: dict[str, Any] = { + "id": self.id, + "user_id": self.user_id, + "thread_id": self.thread_id, + "role": self.role, + "type": self.memory_type, + "content": self.content, + "metadata": self.metadata, + "created_at": self.created_at, + } + if self.embedding is not None: + data["embedding"] = self.embedding + if self.agent_id is not None: + data["agent_id"] = self.agent_id + if self.updated_at is not None: + data["updated_at"] = self.updated_at + return data + + @classmethod + def from_cosmos_dict(cls, doc: dict[str, Any]) -> "MemoryRecord": + """Create a ``MemoryRecord`` from a Cosmos DB document dict. + + Handles the ``"type"`` → ``memory_type`` mapping automatically via + the Pydantic alias. Extra Cosmos system fields (e.g. ``_rid``, + ``_ts``) are silently ignored. + """ + return cls.model_validate(doc) + + +# --------------------------------------------------------------------------- +# Search result wrapper +# --------------------------------------------------------------------------- + + +class SearchResult(BaseModel): + """A memory record returned from a similarity or keyword search.""" + + record: MemoryRecord + score: Optional[float] = None + + +# --------------------------------------------------------------------------- +# Orchestration result +# --------------------------------------------------------------------------- + + +class OrchestrationResult(BaseModel): + """Response envelope for Durable Functions orchestration calls.""" + + runtime_status: str + output: Optional[Any] = None + custom_status: Optional[Any] = None + instance_id: Optional[str] = None diff --git a/agent_memory_toolkit/processing.py b/agent_memory_toolkit/processing.py new file mode 100644 index 0000000..52f541a --- /dev/null +++ b/agent_memory_toolkit/processing.py @@ -0,0 +1,217 @@ +"""Synchronous Azure Durable Functions client for the Agent Memory Toolkit. + +Provides :class:`ProcessingClient` (synchronous, stdlib-only) that +encapsulates the HTTP-start → poll-until-done lifecycle of Durable +Functions orchestrations. +""" + +from __future__ import annotations + +import json as _json +import logging +import time +import urllib.error +import urllib.request +from typing import Any + +from .exceptions import ( + ConfigurationError, + OrchestrationTimeoutError, + ProcessingError, +) + +logger = logging.getLogger(__name__) + +_ORCHESTRATOR_PATH = "/orchestrators/memory_orchestrator" +_TERMINAL_STATUSES = frozenset(("Completed", "Failed", "Terminated")) + + +# --------------------------------------------------------------------------- +# Synchronous client +# --------------------------------------------------------------------------- + + +class ProcessingClient: + """Synchronous Azure Durable Functions client using :mod:`urllib.request`. + + Parameters + ---------- + endpoint: + Base URL of the Azure Functions app hosting the orchestrator. + key: + Optional function-level API key appended as ``?code=…``. + poll_interval: + Seconds between status polls. Defaults to ``2.0``. + timeout: + Maximum seconds to wait for orchestration completion. Defaults to + ``120.0``. + """ + + def __init__( + self, + endpoint: str | None = None, + key: str | None = None, + poll_interval: float = 2.0, + timeout: float = 120.0, + ) -> None: + self._endpoint = endpoint + self._key = key + self._poll_interval = poll_interval + self._timeout = timeout + + # -- core --------------------------------------------------------------- + + def invoke_orchestrator( + self, + payload: dict[str, Any], + poll_interval: float | None = None, + timeout: float | None = None, + ) -> dict[str, Any]: + """Start an orchestration and poll until it reaches a terminal state. + + Parameters + ---------- + payload: + JSON body sent to the orchestrator HTTP-start endpoint. + poll_interval: + Seconds between status polls. Falls back to the constructor value. + timeout: + Maximum seconds to wait. Falls back to the constructor value. + + Returns + ------- + dict + The full status response from the orchestration. + + Raises + ------ + ConfigurationError + If ``endpoint`` is not set. + ProcessingError + If the orchestration finishes with ``runtimeStatus == "Failed"``. + OrchestrationTimeoutError + If polling exceeds *timeout*. + """ + if not self._endpoint: + raise ConfigurationError( + "Processing endpoint is required to invoke orchestrations", + parameter="endpoint", + ) + + poll_interval = poll_interval if poll_interval is not None else self._poll_interval + timeout = timeout if timeout is not None else self._timeout + + url = self._endpoint.rstrip("/") + _ORCHESTRATOR_PATH + if self._key: + url += f"?code={self._key}" + + logger.debug("POST %s with payload %s", url, payload) + + data = _json.dumps(payload).encode("utf-8") + req = urllib.request.Request( + url, data=data, headers={"Content-Type": "application/json"}, method="POST" + ) + try: + with urllib.request.urlopen(req) as resp: + start_response: dict[str, Any] = _json.loads(resp.read().decode("utf-8")) + except urllib.error.HTTPError as exc: + raise ProcessingError( + f"Failed to start orchestration: HTTP {exc.code} {exc.reason}" + ) from exc + except urllib.error.URLError as exc: + raise ProcessingError( + f"Failed to reach orchestration endpoint: {exc.reason}" + ) from exc + + status_url = start_response.get("statusQueryGetUri") + if not status_url: + return start_response + + logger.info( + "Orchestration started (instance=%s), polling for completion", + start_response.get("id"), + ) + + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + time.sleep(poll_interval) + status_req = urllib.request.Request(status_url, method="GET") + try: + with urllib.request.urlopen(status_req) as resp: + status: dict[str, Any] = _json.loads(resp.read().decode("utf-8")) + except urllib.error.HTTPError as exc: + raise ProcessingError( + f"Failed to poll orchestration status: HTTP {exc.code} {exc.reason}" + ) from exc + except urllib.error.URLError as exc: + raise ProcessingError( + f"Failed to reach orchestration status endpoint: {exc.reason}" + ) from exc + + runtime_status = status.get("runtimeStatus", "") + logger.debug("Poll runtimeStatus=%s", runtime_status) + + if runtime_status in _TERMINAL_STATUSES: + if runtime_status == "Failed": + error_detail = status.get("output") or status.get("customStatus") + raise ProcessingError( + f"Orchestration failed: {error_detail}" + ) + logger.info("Orchestration completed with status=%s", runtime_status) + return status + + raise OrchestrationTimeoutError(timeout=timeout, status_url=status_url) + + # -- convenience wrappers ----------------------------------------------- + + def generate_thread_summary( + self, + user_id: str, + thread_id: str, + recent_k: int | None = None, + **kwargs: Any, + ) -> dict[str, Any]: + """Generate a summary for a single thread.""" + payload: dict[str, Any] = { + "user_id": user_id, + "thread_id": thread_id, + "thread_summary_only": True, + } + if recent_k is not None: + payload["recent_k"] = recent_k + return self.invoke_orchestrator(payload, **kwargs) + + def extract_facts( + self, + user_id: str, + thread_id: str, + recent_k: int | None = None, + **kwargs: Any, + ) -> dict[str, Any]: + """Extract factual knowledge from a thread.""" + payload: dict[str, Any] = { + "user_id": user_id, + "thread_id": thread_id, + "extract_facts_only": True, + } + if recent_k is not None: + payload["recent_k"] = recent_k + return self.invoke_orchestrator(payload, **kwargs) + + def generate_user_summary( + self, + user_id: str, + thread_ids: list[str] | None = None, + recent_k: int | None = None, + **kwargs: Any, + ) -> dict[str, Any]: + """Generate a cross-thread summary for a user.""" + payload: dict[str, Any] = { + "user_id": user_id, + "user_summary_only": True, + } + if thread_ids is not None: + payload["thread_ids"] = thread_ids + if recent_k is not None: + payload["recent_k"] = recent_k + return self.invoke_orchestrator(payload, **kwargs) diff --git a/azure_functions/activities.py b/azure_functions/activities.py index 39e1a8c..0beec39 100644 --- a/azure_functions/activities.py +++ b/azure_functions/activities.py @@ -1,26 +1,118 @@ -"""Activity functions for the Agent Memory durable orchestration.""" +"""Activity functions for the Agent Memory durable orchestration. + +Provides Azure Durable Functions activities for memory processing: +load, embed, store, summarize threads, extract facts, and build user profiles. +""" import json +import logging import os +import time import uuid +from collections import defaultdict from datetime import datetime, timezone import azure.durable_functions as df from azure.cosmos import CosmosClient +from azure.cosmos.exceptions import CosmosHttpResponseError, CosmosResourceNotFoundError from azure.identity import DefaultAzureCredential +logger = logging.getLogger(__name__) + bp = df.Blueprint() + # --------------------------------------------------------------------------- -# Shared helpers – lazily initialised singletons +# Shared helpers +# --------------------------------------------------------------------------- + + +def _get_llm_model() -> str: + """Return the configured LLM model name. + + Checks ``LLM_MODEL`` first, then ``AI_FOUNDRY_LLM``, then defaults + to ``"gpt-4o"``. + """ + return os.environ.get("LLM_MODEL") or os.environ.get("AI_FOUNDRY_LLM") or "gpt-4o" + + +def _get_embedding_model() -> str: + return ( + os.environ.get("AI_FOUNDRY_EMBEDDING_MODEL") + or os.environ.get("EMBEDDING_MODEL") + or "text-embedding-3-large" + ) + + +def _get_embedding_dimensions() -> int: + return int(os.environ.get("EMBEDDING_DIMENSION", "1536")) + + +def _build_transcript(items: list[dict], *, group_by_thread: bool = False) -> str: + """Build a formatted transcript from memory documents. + + Args: + items: Memory dicts with ``role``, ``content``, and optional ``metadata``. + group_by_thread: If *True*, group messages under ``=== Thread ===`` + headers. Otherwise produce a flat list. + + Returns: + A newline-joined transcript string. + """ + if not group_by_thread: + lines: list[str] = [] + for m in items: + role = m.get("role", "unknown") + content = m.get("content", "") + metadata = m.get("metadata", {}) + meta_str = f" [metadata: {json.dumps(metadata)}]" if metadata else "" + lines.append(f"[{role}]: {content}{meta_str}") + return "\n".join(lines) + + threads: dict[str, list[dict]] = defaultdict(list) + for m in items: + threads[m.get("thread_id", "")].append(m) + + parts: list[str] = [] + for tid, thread_items in threads.items(): + parts.append(f"=== Thread {tid} ===") + for m in thread_items: + role = m.get("role", "unknown") + content = m.get("content", "") + metadata = m.get("metadata", {}) + meta_str = f" [metadata: {json.dumps(metadata)}]" if metadata else "" + parts.append(f"[{role}]: {content}{meta_str}") + parts.append("") + return "\n".join(parts) + + +def _validate_required(payload: dict, *keys: str, activity: str = "") -> None: + """Raise ``ValueError`` if any *keys* are missing or ``None`` in *payload*.""" + missing = [k for k in keys if payload.get(k) is None] + if missing: + raise ValueError( + f"{activity + ': ' if activity else ''}missing required field(s): {', '.join(missing)}" + ) + + +def _load_prompt(filename: str) -> str: + """Read a prompt file from the ``prompts/`` directory.""" + path = os.path.join(os.path.dirname(__file__), "prompts", filename) + with open(path, "r", encoding="utf-8") as f: + return f.read() + + +# --------------------------------------------------------------------------- +# Lazily initialised singletons # --------------------------------------------------------------------------- _cosmos_container = None _credential = None +_openai_client = None def _get_credential(): - """Return a shared DefaultAzureCredential (Entra ID / MI).""" + """Return a shared ``DefaultAzureCredential``.""" global _credential if _credential is None: _credential = DefaultAzureCredential() @@ -34,27 +126,33 @@ def _get_cosmos_container(): endpoint = os.environ["COSMOS_DB_ENDPOINT"] database = os.environ["COSMOS_DB_DATABASE"] container = os.environ["COSMOS_DB_CONTAINER"] + logger.info( + "Connecting to Cosmos DB endpoint=%s database=%s container=%s", + f"...{endpoint[-8:]}", database, container, + ) client = CosmosClient(endpoint, credential=_get_credential()) db = client.get_database_client(database) _cosmos_container = db.get_container_client(container) return _cosmos_container -_embeddings_client = None - - -def _get_embeddings_client(): - """Return a cached AzureOpenAI client for embeddings.""" - global _embeddings_client - if _embeddings_client is None: +def _get_openai_client(): + """Return a cached ``AzureOpenAI`` client (used for both embeddings and chat).""" + global _openai_client + if _openai_client is None: from openai import AzureOpenAI endpoint = os.environ["AI_FOUNDRY_ENDPOINT"] api_key = os.environ.get("AI_FOUNDRY_API_KEY") api_version = os.environ.get("AI_FOUNDRY_API_VERSION", "2024-12-01-preview") + logger.info( + "Initializing AzureOpenAI client endpoint=%s auth=%s", + f"...{endpoint[-8:]}", "api_key" if api_key else "entra_id", + ) + if api_key: - _embeddings_client = AzureOpenAI( + _openai_client = AzureOpenAI( api_version=api_version, azure_endpoint=endpoint, api_key=api_key, @@ -66,52 +164,188 @@ def _get_embeddings_client(): _get_credential(), "https://cognitiveservices.azure.com/.default", ) - _embeddings_client = AzureOpenAI( + _openai_client = AzureOpenAI( api_version=api_version, azure_endpoint=endpoint, azure_ad_token_provider=token_provider, ) - return _embeddings_client + return _openai_client -_chat_client = None - +# --------------------------------------------------------------------------- +# Resilient LLM / embedding wrappers +# --------------------------------------------------------------------------- -def _get_chat_client(): - """Return a cached AzureOpenAI client for chat completions.""" - global _chat_client - if _chat_client is None: - from openai import AzureOpenAI +_RETRYABLE_STATUS_CODES = (429, 500, 503) + + +def _call_llm_with_retry( + client, + model: str, + messages: list[dict], + *, + max_retries: int = 3, + base_delay: float = 2.0, +): + """Call chat completions with exponential backoff for transient errors.""" + import openai + + for attempt in range(max_retries): + try: + response = client.chat.completions.create(model=model, messages=messages) + usage = getattr(response, "usage", None) + if usage: + logger.info( + "LLM response model=%s prompt_tokens=%s completion_tokens=%s total_tokens=%s", + model, usage.prompt_tokens, usage.completion_tokens, usage.total_tokens, + ) + return response + except openai.RateLimitError as exc: + if attempt < max_retries - 1: + delay = base_delay * (2 ** attempt) + logger.warning( + "LLM rate-limited (attempt %d/%d), retrying in %.1fs: %s", + attempt + 1, max_retries, delay, exc, + ) + time.sleep(delay) + continue + logger.warning("LLM rate-limited after %d attempts, re-raising", max_retries) + raise + except openai.APIError as exc: + status = getattr(exc, "status_code", None) + if status in _RETRYABLE_STATUS_CODES and attempt < max_retries - 1: + delay = base_delay * (2 ** attempt) + logger.warning( + "LLM API error %s (attempt %d/%d), retrying in %.1fs: %s", + status, attempt + 1, max_retries, delay, exc, + ) + time.sleep(delay) + continue + logger.error("LLM API error (status=%s): %s", status, exc, exc_info=True) + raise + except Exception as exc: + logger.error("LLM call failed unexpectedly: %s", exc, exc_info=True) + raise + + +def _generate_embedding( + text: str, + *, + max_retries: int = 3, + base_delay: float = 2.0, +) -> list[float]: + """Generate an embedding for *text* with retry logic. + + Uses the shared ``AzureOpenAI`` client, embedding model, and + dimensions from environment variables. Retries on rate-limit and + transient API errors with exponential backoff. + """ + import openai - endpoint = os.environ["AI_FOUNDRY_ENDPOINT"] - api_key = os.environ.get("AI_FOUNDRY_API_KEY") - api_version = os.environ.get("AI_FOUNDRY_API_VERSION", "2024-12-01-preview") + model = _get_embedding_model() + dimensions = _get_embedding_dimensions() + client = _get_openai_client() - if api_key: - _chat_client = AzureOpenAI( - api_version=api_version, - azure_endpoint=endpoint, - api_key=api_key, + for attempt in range(max_retries): + try: + response = client.embeddings.create( + input=[text], model=model, dimensions=dimensions, ) - else: - from azure.identity import get_bearer_token_provider + return response.data[0].embedding + except openai.RateLimitError as exc: + if attempt < max_retries - 1: + delay = base_delay * (2 ** attempt) + logger.warning( + "Embedding rate-limited (attempt %d/%d), retrying in %.1fs: %s", + attempt + 1, max_retries, delay, exc, + ) + time.sleep(delay) + continue + logger.warning("Embedding rate-limited after %d attempts, re-raising", max_retries) + raise + except openai.APIError as exc: + status = getattr(exc, "status_code", None) + if status in _RETRYABLE_STATUS_CODES and attempt < max_retries - 1: + delay = base_delay * (2 ** attempt) + logger.warning( + "Embedding API error %s (attempt %d/%d), retrying in %.1fs: %s", + status, attempt + 1, max_retries, delay, exc, + ) + time.sleep(delay) + continue + logger.error("Embedding API error (status=%s): %s", status, exc, exc_info=True) + raise + except Exception as exc: + logger.error("Embedding call failed model=%s: %s", model, exc, exc_info=True) + raise + + # Should not be reached, but satisfy type checkers + raise RuntimeError("Embedding generation failed after all retries") + + +def _generate_embeddings_batch( + texts: list[str], + *, + max_retries: int = 3, + base_delay: float = 2.0, +) -> list[list[float]]: + """Generate embeddings for multiple texts in a single API call. + + Uses the OpenAI batch embedding API to reduce latency and rate-limit + pressure compared to sequential per-text calls. + """ + import openai - token_provider = get_bearer_token_provider( - _get_credential(), - "https://cognitiveservices.azure.com/.default", - ) - _chat_client = AzureOpenAI( - api_version=api_version, - azure_endpoint=endpoint, - azure_ad_token_provider=token_provider, + if not texts: + return [] + + model = _get_embedding_model() + dimensions = _get_embedding_dimensions() + client = _get_openai_client() + + for attempt in range(max_retries): + try: + response = client.embeddings.create( + input=texts, model=model, dimensions=dimensions, ) - return _chat_client + # Sort by index to preserve input order + sorted_data = sorted(response.data, key=lambda d: d.index) + return [d.embedding for d in sorted_data] + except openai.RateLimitError as exc: + if attempt < max_retries - 1: + delay = base_delay * (2 ** attempt) + logger.warning( + "Batch embedding rate-limited (attempt %d/%d), retrying in %.1fs: %s", + attempt + 1, max_retries, delay, exc, + ) + time.sleep(delay) + continue + logger.warning("Batch embedding rate-limited after %d attempts, re-raising", max_retries) + raise + except openai.APIError as exc: + status = getattr(exc, "status_code", None) + if status in _RETRYABLE_STATUS_CODES and attempt < max_retries - 1: + delay = base_delay * (2 ** attempt) + logger.warning( + "Batch embedding API error %s (attempt %d/%d), retrying in %.1fs: %s", + status, attempt + 1, max_retries, delay, exc, + ) + time.sleep(delay) + continue + logger.error("Batch embedding API error (status=%s): %s", status, exc, exc_info=True) + raise + except Exception as exc: + logger.error("Batch embedding call failed model=%s: %s", model, exc, exc_info=True) + raise + + raise RuntimeError("Batch embedding generation failed after all retries") # ===================================================================== # Activity: load_memories # ===================================================================== + @bp.activity_trigger(input_name="payload") def load_memories(payload: dict) -> list: """Load all memories for a given thread_id from Cosmos DB. @@ -121,24 +355,37 @@ def load_memories(payload: dict) -> list: Returns a list of memory dicts. """ + _validate_required(payload, "thread_id", activity="load_memories") thread_id = payload["thread_id"] - container = _get_cosmos_container() + logger.info("load_memories started thread_id=%s", thread_id) - query = "SELECT * FROM c WHERE c.thread_id = @thread_id" - parameters = [{"name": "@thread_id", "value": thread_id}] + try: + container = _get_cosmos_container() + query = "SELECT * FROM c WHERE c.thread_id = @thread_id" + parameters = [{"name": "@thread_id", "value": thread_id}] + + items = container.query_items( + query=query, + parameters=parameters, + enable_cross_partition_query=True, + ) + results = list(items) + except CosmosHttpResponseError as exc: + logger.error("load_memories Cosmos query failed: %s", exc, exc_info=True) + raise - items = container.query_items( - query=query, - parameters=parameters, - enable_cross_partition_query=True, - ) - return list(items) + if not results: + logger.warning("load_memories returned 0 results thread_id=%s", thread_id) + else: + logger.info("load_memories completed count=%d thread_id=%s", len(results), thread_id) + return results # ===================================================================== # Activity: generate_embeddings # ===================================================================== + @bp.activity_trigger(input_name="payload") def generate_embeddings(payload: dict) -> list: """Generate a vector embedding for the given text. @@ -148,23 +395,23 @@ def generate_embeddings(payload: dict) -> list: Returns a list of floats (the embedding vector). """ + _validate_required(payload, "text", activity="generate_embeddings") text = payload["text"] - model = os.environ.get("AI_FOUNDRY_EMBEDDING_MODEL", "text-embedding-3-large") - - dimensions = int(os.environ.get("EMBEDDING_DIMENSION", "1536")) - client = _get_embeddings_client() - response = client.embeddings.create( - input=[text], - model=model, - dimensions=dimensions, - ) - return response.data[0].embedding + + model = _get_embedding_model() + dimensions = _get_embedding_dimensions() + logger.info("generate_embeddings started model=%s dimensions=%d", model, dimensions) + + embedding = _generate_embedding(text) + logger.info("generate_embeddings completed vector_length=%d", len(embedding)) + return embedding # ===================================================================== # Activity: store_results # ===================================================================== + @bp.activity_trigger(input_name="payload") def store_results(payload: dict) -> dict: """Store (upsert) a memory document in Cosmos DB. @@ -182,7 +429,8 @@ def store_results(payload: dict) -> dict: Returns the stored document. """ - container = _get_cosmos_container() + _validate_required(payload, "user_id", "thread_id", "content", "embedding", activity="store_results") + logger.info("store_results started input_keys=%s", list(payload.keys())) doc = { "id": str(uuid.uuid4()), @@ -196,7 +444,14 @@ def store_results(payload: dict) -> dict: "created_at": datetime.now(timezone.utc).isoformat(), } - container.upsert_item(body=doc) + try: + container = _get_cosmos_container() + container.upsert_item(body=doc) + except CosmosHttpResponseError as exc: + logger.error("store_results upsert failed id=%s: %s", doc["id"], exc, exc_info=True) + raise + + logger.info("store_results upserted id=%s type=%s", doc["id"], doc["type"]) return doc @@ -204,30 +459,40 @@ def store_results(payload: dict) -> dict: # Activity: generate_thread_summary # ===================================================================== + @bp.activity_trigger(input_name="payload") def generate_thread_summary(payload: dict) -> dict: - """Generate or incrementally update a thread summary using an AI Foundry LLM. + """Generate or incrementally update a thread summary using an LLM. If a summary already exists for the thread, only memories created - *after* the existing summary are fetched. The LLM then receives + *after* the existing summary are fetched. The LLM then receives the old summary together with the new messages and produces an - updated summary. The document is upserted with a deterministic ID + updated summary. The document is upserted with a deterministic ID so there is at most one active summary per thread. Input:: { "user_id": "...", "thread_id": "...", - "recent_k": 10 # optional – per-thread recency limit + "recent_k": 10 # optional -- per-thread recency limit } """ - from azure.cosmos.exceptions import CosmosResourceNotFoundError + _validate_required(payload, "user_id", "thread_id", activity="generate_thread_summary") user_id = payload["user_id"] thread_id = payload["thread_id"] recent_k = payload.get("recent_k") - model = os.environ.get("LLM_MODEL", "gpt-5-nano") - container = _get_cosmos_container() + model = _get_llm_model() + logger.info( + "generate_thread_summary started user_id=%s thread_id=%s model=%s", + user_id, thread_id, model, + ) + + try: + container = _get_cosmos_container() + except CosmosHttpResponseError as exc: + logger.error("generate_thread_summary Cosmos connection failed: %s", exc, exc_info=True) + raise # ---- 1. Check for an existing thread summary ---- existing_summary = None @@ -238,7 +503,10 @@ def generate_thread_summary(payload: dict) -> dict: partition_key=[user_id, thread_id], ) except CosmosResourceNotFoundError: - pass # first time – full generation + pass # first time -- full generation + except CosmosHttpResponseError as exc: + logger.error("generate_thread_summary read existing summary failed: %s", exc, exc_info=True) + raise # ---- 2. Query memories (time-filtered if updating) ---- query = ( @@ -256,17 +524,24 @@ def generate_thread_summary(payload: dict) -> dict: query += " AND c.created_at > @since" parameters.append({"name": "@since", "value": since}) - items = list(container.query_items( - query=query, - parameters=parameters, - enable_cross_partition_query=True, - )) + try: + items = list(container.query_items( + query=query, + parameters=parameters, + enable_cross_partition_query=True, + )) + except CosmosHttpResponseError as exc: + logger.error("generate_thread_summary query failed: %s", exc, exc_info=True) + raise + + logger.debug("generate_thread_summary query returned %d memories", len(items)) - # If updating and there are no new memories, return the existing summary if existing_summary and not items: + logger.info("generate_thread_summary no new memories, returning existing summary") return existing_summary if not existing_summary and not items: + logger.warning("generate_thread_summary no memories found user_id=%s thread_id=%s", user_id, thread_id) raise ValueError( f"No memories found for user_id={user_id!r}, thread_id={thread_id!r}" ) @@ -278,34 +553,24 @@ def generate_thread_summary(payload: dict) -> dict: items.reverse() # chronological order # ---- 4. Build transcript ---- - transcript_lines = [] - for m in items: - role = m.get("role", "unknown") - content = m.get("content", "") - metadata = m.get("metadata", {}) - meta_str = f" [metadata: {json.dumps(metadata)}]" if metadata else "" - transcript_lines.append(f"[{role}]: {content}{meta_str}") - transcript = "\n".join(transcript_lines) + transcript = _build_transcript(items) # ---- 5. Call LLM (full or incremental prompt) ---- if existing_summary: - prompt_file = "summarize_update.md" + system_prompt = _load_prompt("summarize_update.md") user_message = ( f"## Existing Summary\n\n{existing_summary['content']}\n\n" f"## New Messages\n\n{transcript}" ) else: - prompt_file = "summarize.md" + system_prompt = _load_prompt("summarize.md") user_message = transcript - prompt_path = os.path.join(os.path.dirname(__file__), "prompts", prompt_file) - with open(prompt_path, "r", encoding="utf-8") as f: - system_prompt = f.read() - - client = _get_chat_client() - response = client.chat.completions.create( - model=model, - messages=[ + logger.info("generate_thread_summary calling LLM model=%s", model) + client = _get_openai_client() + response = _call_llm_with_retry( + client, model, + [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_message}, ], @@ -313,15 +578,8 @@ def generate_thread_summary(payload: dict) -> dict: summary_text = response.choices[0].message.content # ---- 6. Generate embedding ---- - embedding_model = os.environ.get("AI_FOUNDRY_EMBEDDING_MODEL", "text-embedding-3-large") - dimensions = int(os.environ.get("EMBEDDING_DIMENSION", "1536")) - emb_client = _get_embeddings_client() - emb_response = emb_client.embeddings.create( - input=[summary_text], - model=embedding_model, - dimensions=dimensions, - ) - summary_embedding = emb_response.data[0].embedding + logger.info("generate_thread_summary generating embedding") + summary_embedding = _generate_embedding(summary_text) # ---- 7. Upsert summary (deterministic ID, accumulate counts) ---- if existing_summary: @@ -345,8 +603,14 @@ def generate_thread_summary(payload: dict) -> dict: }, "created_at": datetime.now(timezone.utc).isoformat(), } - container.upsert_item(body=summary_doc) + try: + container.upsert_item(body=summary_doc) + except CosmosHttpResponseError as exc: + logger.error("generate_thread_summary upsert failed: %s", exc, exc_info=True) + raise + + logger.info("generate_thread_summary completed id=%s source_count=%d", summary_id, total_source_count) return summary_doc @@ -354,15 +618,16 @@ def generate_thread_summary(payload: dict) -> dict: # Activity: extract_facts # ===================================================================== + @bp.activity_trigger(input_name="payload") def extract_facts(payload: dict) -> dict: - """Extract facts from a user's thread memories using an AI Foundry LLM. + """Extract facts from a user's thread memories using an LLM. Input:: { "user_id": "...", "thread_id": "...", - "recent_k": 10 # optional – keep only the most recent k + "recent_k": 10 # optional -- keep only the most recent k } Steps: @@ -370,29 +635,36 @@ def extract_facts(payload: dict) -> dict: 2. Sort by created_at descending; if recent_k is set, keep only the most recent k documents. 3. Extract content and metadata, send to the LLM for fact extraction. - 4. Insert the facts back into Cosmos DB as a memory of type "fact". - 5. Return the stored fact document. + 4. Insert the facts back into Cosmos DB as a memory of type ``"fact"``. + 5. Return the stored fact documents. """ + _validate_required(payload, "user_id", "thread_id", activity="extract_facts") + user_id = payload["user_id"] thread_id = payload["thread_id"] recent_k = payload.get("recent_k") - model = os.environ.get("AI_FOUNDRY_LLM", "gpt-5-nano") + model = _get_llm_model() + logger.info("extract_facts started user_id=%s thread_id=%s model=%s", user_id, thread_id, model) # ---- 1. Query Cosmos DB ---- - container = _get_cosmos_container() - query = ( - "SELECT * FROM c " - "WHERE c.user_id = @user_id AND c.thread_id = @thread_id" - ) - parameters = [ - {"name": "@user_id", "value": user_id}, - {"name": "@thread_id", "value": thread_id}, - ] - items = list(container.query_items( - query=query, - parameters=parameters, - enable_cross_partition_query=True, - )) + try: + container = _get_cosmos_container() + query = ( + "SELECT * FROM c " + "WHERE c.user_id = @user_id AND c.thread_id = @thread_id" + ) + parameters = [ + {"name": "@user_id", "value": user_id}, + {"name": "@thread_id", "value": thread_id}, + ] + items = list(container.query_items( + query=query, + parameters=parameters, + enable_cross_partition_query=True, + )) + except CosmosHttpResponseError as exc: + logger.error("extract_facts Cosmos query failed: %s", exc, exc_info=True) + raise # ---- 2. Sort and trim ---- items.sort(key=lambda m: m.get("created_at", ""), reverse=True) @@ -401,28 +673,20 @@ def extract_facts(payload: dict) -> dict: items.reverse() if not items: + logger.warning("extract_facts no memories found user_id=%s thread_id=%s", user_id, thread_id) raise ValueError( f"No memories found for user_id={user_id!r}, thread_id={thread_id!r}" ) # ---- 3. Build transcript and call LLM ---- - transcript_lines = [] - for m in items: - role = m.get("role", "unknown") - content = m.get("content", "") - metadata = m.get("metadata", {}) - meta_str = f" [metadata: {json.dumps(metadata)}]" if metadata else "" - transcript_lines.append(f"[{role}]: {content}{meta_str}") - transcript = "\n".join(transcript_lines) - - prompt_path = os.path.join(os.path.dirname(__file__), "prompts", "facts.md") - with open(prompt_path, "r", encoding="utf-8") as f: - system_prompt = f.read() - - client = _get_chat_client() - response = client.chat.completions.create( - model=model, - messages=[ + transcript = _build_transcript(items) + system_prompt = _load_prompt("facts.md") + + logger.info("extract_facts calling LLM model=%s", model) + client = _get_openai_client() + response = _call_llm_with_retry( + client, model, + [ {"role": "system", "content": system_prompt}, {"role": "user", "content": transcript}, ], @@ -439,19 +703,14 @@ def extract_facts(payload: dict) -> dict: if not fact_lines: fact_lines = [facts_text.strip()] - # ---- 5. Generate embeddings and store each fact ---- - embedding_model = os.environ.get("AI_FOUNDRY_EMBEDDING_MODEL", "text-embedding-3-large") - dimensions = int(os.environ.get("EMBEDDING_DIMENSION", "1536")) - emb_client = _get_embeddings_client() + # ---- 5. Generate embeddings in batch and store each fact ---- + logger.info("extract_facts generating embeddings for %d facts", len(fact_lines)) now = datetime.now(timezone.utc).isoformat() + fact_embeddings = _generate_embeddings_batch(fact_lines) facts_docs = [] - for fact in fact_lines: - emb_response = emb_client.embeddings.create( - input=[fact], - model=embedding_model, - dimensions=dimensions, - ) + for fact, fact_embedding in zip(fact_lines, fact_embeddings): + fact_doc = { "id": str(uuid.uuid4()), "user_id": user_id, @@ -459,16 +718,23 @@ def extract_facts(payload: dict) -> dict: "role": "system", "type": "fact", "content": fact, - "embedding": emb_response.data[0].embedding, + "embedding": fact_embedding, "metadata": { "source_count": len(items), "recent_k": recent_k, }, "created_at": now, } - container.upsert_item(body=fact_doc) + + try: + container.upsert_item(body=fact_doc) + except CosmosHttpResponseError as exc: + logger.error("extract_facts upsert failed fact_id=%s: %s", fact_doc["id"], exc, exc_info=True) + raise + facts_docs.append(fact_doc) + logger.info("extract_facts completed facts_count=%d", len(facts_docs)) return facts_docs @@ -476,30 +742,36 @@ def extract_facts(payload: dict) -> dict: # Activity: generate_user_summary # ===================================================================== + @bp.activity_trigger(input_name="payload") def generate_user_summary(payload: dict) -> dict: """Generate or incrementally update a cross-thread user summary. If a user summary already exists, only memories created *after* the - existing summary are fetched. The LLM then receives the old profile + existing summary are fetched. The LLM then receives the old profile together with the new conversation data and produces an updated - profile. Thread IDs and memory counts are accumulated across runs. + profile. Thread IDs and memory counts are accumulated across runs. Input:: { "user_id": "...", - "thread_ids": ["..."], # optional – limit to specific threads - "recent_k": 10 # optional – per-thread recency limit + "thread_ids": ["..."], # optional -- limit to specific threads + "recent_k": 10 # optional -- per-thread recency limit } """ - from collections import defaultdict - from azure.cosmos.exceptions import CosmosResourceNotFoundError + _validate_required(payload, "user_id", activity="generate_user_summary") user_id = payload["user_id"] thread_ids = payload.get("thread_ids") recent_k = payload.get("recent_k") - model = os.environ.get("AI_FOUNDRY_LLM", "gpt-5-nano") - container = _get_cosmos_container() + model = _get_llm_model() + logger.info("generate_user_summary started user_id=%s model=%s", user_id, model) + + try: + container = _get_cosmos_container() + except CosmosHttpResponseError as exc: + logger.error("generate_user_summary Cosmos connection failed: %s", exc, exc_info=True) + raise # ---- 1. Check for an existing user summary ---- existing_summary = None @@ -509,7 +781,10 @@ def generate_user_summary(payload: dict) -> dict: partition_key=[user_id, "__user_summary__"], ) except CosmosResourceNotFoundError: - pass # first time – full generation + pass # first time -- full generation + except CosmosHttpResponseError as exc: + logger.error("generate_user_summary read existing summary failed: %s", exc, exc_info=True) + raise # ---- 2. Query memories (time-filtered if updating) ---- query = ( @@ -526,27 +801,30 @@ def generate_user_summary(payload: dict) -> dict: parameters.append({"name": "@since", "value": since}) if thread_ids: - placeholders = ", ".join( - f"@tid{i}" for i in range(len(thread_ids)) - ) + placeholders = ", ".join(f"@tid{i}" for i in range(len(thread_ids))) query += f" AND c.thread_id IN ({placeholders})" for i, tid in enumerate(thread_ids): parameters.append({"name": f"@tid{i}", "value": tid}) - items = list(container.query_items( - query=query, - parameters=parameters, - enable_cross_partition_query=True, - )) + try: + items = list(container.query_items( + query=query, + parameters=parameters, + enable_cross_partition_query=True, + )) + except CosmosHttpResponseError as exc: + logger.error("generate_user_summary query failed: %s", exc, exc_info=True) + raise + + logger.debug("generate_user_summary query returned %d memories", len(items)) - # If updating and there are no new memories, return the existing summary if existing_summary and not items: + logger.info("generate_user_summary no new memories, returning existing summary") return existing_summary if not existing_summary and not items: - raise ValueError( - f"No memories found for user_id={user_id!r}" - ) + logger.warning("generate_user_summary no memories found user_id=%s", user_id) + raise ValueError(f"No memories found for user_id={user_id!r}") # ---- 3. Sort and apply per-thread recent_k trimming ---- items.sort(key=lambda m: m.get("created_at", ""), reverse=True) @@ -555,7 +833,7 @@ def generate_user_summary(payload: dict) -> dict: by_thread: dict[str, list] = defaultdict(list) for m in items: by_thread[m.get("thread_id", "")].append(m) - trimmed = [] + trimmed: list[dict] = [] for thread_items in by_thread.values(): trimmed.extend(thread_items[:recent_k]) trimmed.sort(key=lambda m: m.get("created_at", "")) @@ -564,41 +842,27 @@ def generate_user_summary(payload: dict) -> dict: items.reverse() # chronological order # ---- 4. Build transcript grouped by thread ---- - threads: dict[str, list] = defaultdict(list) - for m in items: - threads[m.get("thread_id", "")].append(m) + transcript = _build_transcript(items, group_by_thread=True) - transcript_parts = [] - for tid, thread_items in threads.items(): - transcript_parts.append(f"=== Thread {tid} ===") - for m in thread_items: - role = m.get("role", "unknown") - content = m.get("content", "") - metadata = m.get("metadata", {}) - meta_str = f" [metadata: {json.dumps(metadata)}]" if metadata else "" - transcript_parts.append(f"[{role}]: {content}{meta_str}") - transcript_parts.append("") - transcript = "\n".join(transcript_parts) + # Collect thread IDs from items for metadata + new_thread_ids = {m.get("thread_id", "") for m in items} # ---- 5. Call LLM (full or incremental prompt) ---- if existing_summary: - prompt_file = "user_summary_update.md" + system_prompt = _load_prompt("user_summary_update.md") user_message = ( f"## Existing Profile\n\n{existing_summary['content']}\n\n" f"## New Conversations\n\n{transcript}" ) else: - prompt_file = "user_summary.md" + system_prompt = _load_prompt("user_summary.md") user_message = transcript - prompt_path = os.path.join(os.path.dirname(__file__), "prompts", prompt_file) - with open(prompt_path, "r", encoding="utf-8") as f: - system_prompt = f.read() - - client = _get_chat_client() - response = client.chat.completions.create( - model=model, - messages=[ + logger.info("generate_user_summary calling LLM model=%s", model) + client = _get_openai_client() + response = _call_llm_with_retry( + client, model, + [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_message}, ], @@ -606,18 +870,10 @@ def generate_user_summary(payload: dict) -> dict: summary_text = response.choices[0].message.content # ---- 6. Generate embedding ---- - embedding_model = os.environ.get("AI_FOUNDRY_EMBEDDING_MODEL", "text-embedding-3-large") - dimensions = int(os.environ.get("EMBEDDING_DIMENSION", "1536")) - emb_client = _get_embeddings_client() - emb_response = emb_client.embeddings.create( - input=[summary_text], - model=embedding_model, - dimensions=dimensions, - ) - summary_embedding = emb_response.data[0].embedding + logger.info("generate_user_summary generating embedding") + summary_embedding = _generate_embedding(summary_text) # ---- 7. Upsert user summary (accumulate thread IDs and counts) ---- - new_thread_ids = set(threads.keys()) if existing_summary: old_thread_ids = set(existing_summary.get("metadata", {}).get("thread_ids", [])) all_thread_ids = sorted(old_thread_ids | new_thread_ids) @@ -644,6 +900,15 @@ def generate_user_summary(payload: dict) -> dict: }, "created_at": datetime.now(timezone.utc).isoformat(), } - container.upsert_item(body=summary_doc) + try: + container.upsert_item(body=summary_doc) + except CosmosHttpResponseError as exc: + logger.error("generate_user_summary upsert failed: %s", exc, exc_info=True) + raise + + logger.info( + "generate_user_summary completed thread_count=%d memory_count=%d", + len(all_thread_ids), total_memory_count, + ) return summary_doc diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9aa27cb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,48 @@ +[build-system] +requires = ["setuptools>=68.0", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +include = ["agent_memory_toolkit*"] + +[project] +name = "agent-memory-toolkit" +version = "0.1.0" +description = "A Python library for storing, retrieving, and transforming AI agent memories backed by Azure Cosmos DB" +readme = "README.md" +license = {file = "LICENSE"} +requires-python = ">=3.11" +dependencies = [ + "azure-cosmos>=4.15.0", + "azure-identity>=1.17", + "openai>=1.40", + "aiohttp>=3.9", + "pydantic>=2.0", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0", + "pytest-asyncio>=0.23", + "pytest-cov>=5.0", + "pytest-mock>=3.12", + "ruff>=0.4", +] + +[tool.pytest.ini_options] +testpaths = ["tests"] +asyncio_mode = "auto" +markers = [ + "integration: requires live Azure services", + "e2e: requires full pipeline (Cosmos + ADF + AI)", +] + +[tool.ruff] +target-version = "py311" +line-length = 120 + +[tool.ruff.lint] +select = ["E", "F", "I", "W"] + +[tool.coverage.run] +source = ["agent_memory_toolkit"] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 9d914fb..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -azure-cosmos -azure-identity -openai -aiohttp diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..ebdf264 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,82 @@ +"""Shared test fixtures for Agent Memory Toolkit tests.""" + +import uuid +from datetime import datetime, timezone +from unittest.mock import MagicMock + +import pytest + +# --------------------------------------------------------------------------- +# Sample data factories +# --------------------------------------------------------------------------- + +@pytest.fixture +def sample_user_id(): + return "test-user-001" + +@pytest.fixture +def sample_thread_id(): + return str(uuid.uuid4()) + +@pytest.fixture +def sample_memory_dict(sample_user_id, sample_thread_id): + """A raw memory dict as returned by _make_memory or Cosmos queries.""" + return { + "id": str(uuid.uuid4()), + "user_id": sample_user_id, + "thread_id": sample_thread_id, + "role": "user", + "type": "turn", + "content": "What is the weather in Seattle?", + "metadata": {}, + "created_at": datetime.now(timezone.utc).isoformat(), + } + +@pytest.fixture +def sample_memory_dicts(sample_user_id, sample_thread_id): + """A list of memory dicts simulating a conversation thread.""" + now = datetime.now(timezone.utc) + return [ + { + "id": str(uuid.uuid4()), + "user_id": sample_user_id, + "thread_id": sample_thread_id, + "role": "user", + "type": "turn", + "content": "What is the weather in Seattle?", + "metadata": {}, + "created_at": now.isoformat(), + }, + { + "id": str(uuid.uuid4()), + "user_id": sample_user_id, + "thread_id": sample_thread_id, + "role": "agent", + "type": "turn", + "content": "The current weather in Seattle is 55°F and cloudy.", + "metadata": {}, + "created_at": now.isoformat(), + }, + { + "id": str(uuid.uuid4()), + "user_id": sample_user_id, + "thread_id": sample_thread_id, + "role": "user", + "type": "turn", + "content": "Can you book me a hotel near Pike Place Market?", + "metadata": {}, + "created_at": now.isoformat(), + }, + ] + +@pytest.fixture +def sample_embedding(): + """A fake embedding vector (10 dimensions for speed).""" + return [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0] + +@pytest.fixture +def mock_credential(): + """A mock Azure TokenCredential.""" + cred = MagicMock() + cred.get_token.return_value = MagicMock(token="fake-token", expires_on=9999999999) + return cred diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/aio/__init__.py b/tests/unit/aio/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/aio/test_cosmos_memory_client.py b/tests/unit/aio/test_cosmos_memory_client.py new file mode 100644 index 0000000..ca86c8e --- /dev/null +++ b/tests/unit/aio/test_cosmos_memory_client.py @@ -0,0 +1,399 @@ +"""Unit tests for AsyncCosmosMemoryStore.""" + +import uuid +from datetime import datetime, timezone +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from agent_memory_toolkit.aio.cosmos_memory_client import AsyncCosmosMemoryStore +from agent_memory_toolkit.exceptions import ( + ConfigurationError, + CosmosNotConnectedError, + CosmosOperationError, + MemoryNotFoundError, +) +from agent_memory_toolkit.models import MemoryRecord + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +class AsyncIterator: + """Simple async iterator over a list of items.""" + + def __init__(self, items): + self._items = iter(items) + + def __aiter__(self): + return self + + async def __anext__(self): + try: + return next(self._items) + except StopIteration: + raise StopAsyncIteration + + +def _make_record(**overrides) -> MemoryRecord: + defaults = { + "id": str(uuid.uuid4()), + "user_id": "u1", + "thread_id": "t1", + "role": "user", + "content": "hello", + } + defaults.update(overrides) + return MemoryRecord(**defaults) + + +def _make_doc(**overrides) -> dict: + defaults = { + "id": str(uuid.uuid4()), + "user_id": "u1", + "thread_id": "t1", + "role": "user", + "type": "turn", + "content": "hello", + "metadata": {}, + "created_at": datetime.now(timezone.utc).isoformat(), + } + defaults.update(overrides) + return defaults + + +@pytest.fixture +def store(): + return AsyncCosmosMemoryStore( + endpoint="https://fake.documents.azure.com:443/", + credential=MagicMock(), + database="testdb", + container="testcont", + ) + + +@pytest.fixture +def connected_store(store): + """A store with a mocked container client already attached.""" + store._cosmos_client = MagicMock() + store._container_client = MagicMock() + # Make upsert_item, replace_item, delete_item async + store._container_client.upsert_item = AsyncMock() + store._container_client.replace_item = AsyncMock() + store._container_client.delete_item = AsyncMock() + return store + + +# =================================================================== +# connect() +# =================================================================== + + +async def test_connect_success(store): + mock_cosmos_cls = MagicMock() + mock_db = MagicMock() + mock_container = MagicMock() + mock_cosmos_cls.return_value = mock_cosmos_cls + mock_cosmos_cls.get_database_client.return_value = mock_db + mock_db.get_container_client.return_value = mock_container + + with patch( + "agent_memory_toolkit.aio.cosmos_memory_client.CosmosClient", + mock_cosmos_cls, + create=True, + ), patch.dict( + "sys.modules", + {"azure.cosmos.aio": MagicMock(CosmosClient=mock_cosmos_cls)}, + ): + await store.connect() + + assert store._cosmos_client is not None + assert store._container_client is not None + + +async def test_connect_missing_endpoint(): + store = AsyncCosmosMemoryStore(endpoint=None, credential=MagicMock()) + with pytest.raises(ConfigurationError): + await store.connect() + + +async def test_connect_missing_credential(): + store = AsyncCosmosMemoryStore( + endpoint="https://x.documents.azure.com:443/", credential=None + ) + with pytest.raises(ConfigurationError): + await store.connect() + + +# =================================================================== +# _require_connected() +# =================================================================== + + +async def test_require_connected_before_connect(store): + with pytest.raises(CosmosNotConnectedError): + store._require_connected() + + +async def test_require_connected_after_connect(connected_store): + # Should not raise + connected_store._require_connected() + + +# =================================================================== +# upsert() +# =================================================================== + + +async def test_upsert_single(connected_store): + record = _make_record() + await connected_store.upsert(record) + connected_store._container_client.upsert_item.assert_awaited_once() + body = connected_store._container_client.upsert_item.call_args.kwargs["body"] + assert body["id"] == record.id + + +async def test_upsert_not_connected(store): + with pytest.raises(CosmosNotConnectedError): + await store.upsert(_make_record()) + + +async def test_upsert_cosmos_failure(connected_store): + connected_store._container_client.upsert_item.side_effect = Exception("boom") + with pytest.raises(CosmosOperationError): + await connected_store.upsert(_make_record()) + + +# =================================================================== +# upsert_batch() +# =================================================================== + + +async def test_upsert_batch(connected_store): + records = [_make_record() for _ in range(5)] + await connected_store.upsert_batch(records, batch_size=2) + assert connected_store._container_client.upsert_item.await_count == 5 + + +async def test_upsert_batch_single_batch(connected_store): + records = [_make_record() for _ in range(3)] + await connected_store.upsert_batch(records, batch_size=10) + assert connected_store._container_client.upsert_item.await_count == 3 + + +# =================================================================== +# get_memories() +# =================================================================== + + +async def test_get_memories_no_filters(connected_store): + docs = [_make_doc(), _make_doc()] + connected_store._container_client.query_items = MagicMock( + return_value=AsyncIterator(docs) + ) + results = await connected_store.get_memories() + assert len(results) == 2 + connected_store._container_client.query_items.assert_called_once() + + +async def test_get_memories_with_filters(connected_store): + docs = [_make_doc(user_id="u1")] + connected_store._container_client.query_items = MagicMock( + return_value=AsyncIterator(docs) + ) + results = await connected_store.get_memories(user_id="u1", role="user") + assert len(results) == 1 + call_kwargs = connected_store._container_client.query_items.call_args.kwargs + assert "@user_id" in str(call_kwargs["parameters"]) + + +async def test_get_memories_recent_k(connected_store): + docs = [_make_doc(content="older"), _make_doc(content="newer")] + connected_store._container_client.query_items = MagicMock( + return_value=AsyncIterator(docs) + ) + results = await connected_store.get_memories(recent_k=2) + # recent_k reverses the result so oldest-first + assert results[0]["content"] == "newer" + assert results[1]["content"] == "older" + query = connected_store._container_client.query_items.call_args.kwargs["query"] + assert "TOP @recent_k" in query + + +async def test_get_memories_not_connected(store): + with pytest.raises(CosmosNotConnectedError): + await store.get_memories() + + +# =================================================================== +# get_thread() +# =================================================================== + + +async def test_get_thread(connected_store): + docs = [_make_doc(content="second"), _make_doc(content="first")] + connected_store._container_client.query_items = MagicMock( + return_value=AsyncIterator(docs) + ) + results = await connected_store.get_thread(thread_id="t1") + # get_thread reverses to chronological + assert results[0]["content"] == "first" + assert results[1]["content"] == "second" + + +async def test_get_thread_with_recent_k(connected_store): + docs = [_make_doc(content="c"), _make_doc(content="b"), _make_doc(content="a")] + connected_store._container_client.query_items = MagicMock( + return_value=AsyncIterator(docs) + ) + results = await connected_store.get_thread(thread_id="t1", recent_k=2) + # Slices first 2 then reverses + assert len(results) == 2 + + +# =================================================================== +# update() +# =================================================================== + + +async def test_update_success(connected_store): + doc = _make_doc(id="m1") + connected_store._container_client.query_items = MagicMock( + return_value=AsyncIterator([doc]) + ) + await connected_store.update(memory_id="m1", content="updated") + connected_store._container_client.replace_item.assert_awaited_once() + call_kwargs = connected_store._container_client.replace_item.call_args.kwargs + assert call_kwargs["body"]["content"] == "updated" + + +async def test_update_not_found(connected_store): + connected_store._container_client.query_items = MagicMock( + return_value=AsyncIterator([]) + ) + with pytest.raises(MemoryNotFoundError): + await connected_store.update(memory_id="missing") + + +async def test_update_partial_fields(connected_store): + doc = _make_doc(id="m1", role="user", content="old") + connected_store._container_client.query_items = MagicMock( + return_value=AsyncIterator([doc]) + ) + await connected_store.update(memory_id="m1", role="agent", metadata={"key": "val"}) + body = connected_store._container_client.replace_item.call_args.kwargs["body"] + assert body["role"] == "agent" + assert body["metadata"] == {"key": "val"} + assert body["content"] == "old" # unchanged + assert "updated_at" in body + + +# =================================================================== +# delete() +# =================================================================== + + +async def test_delete_success(connected_store): + doc = _make_doc(id="m1", user_id="u1", thread_id="t1") + connected_store._container_client.query_items = MagicMock( + return_value=AsyncIterator([doc]) + ) + await connected_store.delete(memory_id="m1", user_id="u1", thread_id="t1") + connected_store._container_client.delete_item.assert_awaited_once_with( + item="m1", partition_key=["u1", "t1"] + ) + + +async def test_delete_not_found(connected_store): + connected_store._container_client.query_items = MagicMock( + return_value=AsyncIterator([]) + ) + with pytest.raises(MemoryNotFoundError): + await connected_store.delete(memory_id="x", user_id="u1", thread_id="t1") + + +# =================================================================== +# close() +# =================================================================== + + +async def test_close(connected_store): + mock_client = AsyncMock() + connected_store._cosmos_client = mock_client + await connected_store.close() + mock_client.close.assert_awaited_once() + assert connected_store._cosmos_client is None + assert connected_store._container_client is None + + +async def test_close_calls_cosmos_client(): + store = AsyncCosmosMemoryStore( + endpoint="https://fake.documents.azure.com:443/", + credential=MagicMock(), + ) + mock_client = AsyncMock() + store._cosmos_client = mock_client + store._container_client = MagicMock() + await store.close() + mock_client.close.assert_awaited_once() + assert store._cosmos_client is None + assert store._container_client is None + + +async def test_close_noop_when_not_connected(store): + await store.close() # should not raise + + +# =================================================================== +# async context manager +# =================================================================== + + +async def test_context_manager(): + store = AsyncCosmosMemoryStore( + endpoint="https://fake.documents.azure.com:443/", + credential=MagicMock(), + ) + mock_client = AsyncMock() + store._cosmos_client = mock_client + store._container_client = MagicMock() + + async with store as s: + assert s is store + mock_client.close.assert_awaited_once() + + +# =================================================================== +# vector_search() +# =================================================================== + + +async def test_vector_search(connected_store): + docs = [_make_doc(content="result")] + connected_store._container_client.query_items = MagicMock( + return_value=AsyncIterator(docs) + ) + results = await connected_store.vector_search( + query_vector=[0.1, 0.2], user_id="u1", top_k=3 + ) + assert len(results) == 1 + query = connected_store._container_client.query_items.call_args.kwargs["query"] + assert "VectorDistance" in query + + +async def test_vector_search_hybrid(connected_store): + docs = [_make_doc()] + connected_store._container_client.query_items = MagicMock( + return_value=AsyncIterator(docs) + ) + results = await connected_store.vector_search( + query_vector=[0.1], + hybrid_search=True, + search_terms="weather", + top_k=5, + ) + assert len(results) == 1 + query = connected_store._container_client.query_items.call_args.kwargs["query"] + assert "RRF" in query + assert "FullTextScore" in query diff --git a/tests/unit/aio/test_embeddings.py b/tests/unit/aio/test_embeddings.py new file mode 100644 index 0000000..fe5ea84 --- /dev/null +++ b/tests/unit/aio/test_embeddings.py @@ -0,0 +1,241 @@ +"""Unit tests for AsyncEmbeddingsClient.""" + +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from agent_memory_toolkit.aio.embeddings import AsyncEmbeddingsClient +from agent_memory_toolkit.exceptions import ConfigurationError, EmbeddingError + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_embedding_response(embeddings: list[list[float]]): + """Build a mock response matching openai's embedding response shape.""" + data = [] + for i, emb in enumerate(embeddings): + item = MagicMock() + item.embedding = emb + item.index = i + data.append(item) + resp = MagicMock() + resp.data = data + return resp + + +@pytest.fixture +def client(): + return AsyncEmbeddingsClient( + endpoint="https://fake.openai.azure.com/", + api_key="fake-key", + model="text-embedding-3-large", + ) + + +# =================================================================== +# generate() — success +# =================================================================== + + +async def test_generate_success(client): + expected = [0.1, 0.2, 0.3] + mock_openai = MagicMock() + mock_openai.embeddings.create = AsyncMock( + return_value=_make_embedding_response([expected]) + ) + client._client = mock_openai + + result = await client.generate("hello") + assert result == expected + mock_openai.embeddings.create.assert_awaited_once() + + +# =================================================================== +# generate() — lazy init +# =================================================================== + + +async def test_generate_lazy_init(): + """Client is created on first call, not at construction.""" + client = AsyncEmbeddingsClient( + endpoint="https://fake.openai.azure.com/", + api_key="test-key", + ) + assert client._client is None + + mock_cls = MagicMock() + mock_instance = MagicMock() + mock_instance.embeddings.create = AsyncMock( + return_value=_make_embedding_response([[1.0, 2.0]]) + ) + mock_cls.return_value = mock_instance + + with patch("openai.AsyncAzureOpenAI", mock_cls): + result = await client.generate("test") + + assert result == [1.0, 2.0] + assert client._client is mock_instance + mock_cls.assert_called_once() + + +# =================================================================== +# generate() — api_key vs credential auth +# =================================================================== + + +async def test_generate_api_key_auth(): + client = AsyncEmbeddingsClient( + endpoint="https://fake.openai.azure.com/", + api_key="my-key", + ) + mock_cls = MagicMock() + mock_instance = MagicMock() + mock_instance.embeddings.create = AsyncMock( + return_value=_make_embedding_response([[1.0]]) + ) + mock_cls.return_value = mock_instance + + with patch("openai.AsyncAzureOpenAI", mock_cls): + await client.generate("x") + + call_kwargs = mock_cls.call_args.kwargs + assert call_kwargs["api_key"] == "my-key" + assert "azure_ad_token_provider" not in call_kwargs + + +async def test_generate_credential_auth(): + mock_cred = MagicMock() + client = AsyncEmbeddingsClient( + endpoint="https://fake.openai.azure.com/", + credential=mock_cred, + ) + + mock_cls = MagicMock() + mock_instance = MagicMock() + mock_instance.embeddings.create = AsyncMock( + return_value=_make_embedding_response([[2.0]]) + ) + mock_cls.return_value = mock_instance + + mock_token_provider = MagicMock() + with patch("openai.AsyncAzureOpenAI", mock_cls), \ + patch("azure.identity.aio.get_bearer_token_provider", + return_value=mock_token_provider): + await client.generate("x") + + call_kwargs = mock_cls.call_args.kwargs + assert "azure_ad_token_provider" in call_kwargs + assert "api_key" not in call_kwargs + + +# =================================================================== +# generate() — missing endpoint +# =================================================================== + + +async def test_generate_missing_endpoint(): + client = AsyncEmbeddingsClient(endpoint=None, api_key="key") + with pytest.raises(ConfigurationError): + await client.generate("hello") + + +async def test_generate_missing_credential_and_api_key(): + client = AsyncEmbeddingsClient( + endpoint="https://fake.openai.azure.com/", + credential=None, + api_key=None, + ) + with pytest.raises(ConfigurationError): + await client.generate("hello") + + +# =================================================================== +# generate() — API failure +# =================================================================== + + +async def test_generate_api_failure(client): + mock_openai = MagicMock() + mock_openai.embeddings.create = AsyncMock(side_effect=Exception("API down")) + client._client = mock_openai + + with pytest.raises(EmbeddingError, match="API down"): + await client.generate("hello") + + +# =================================================================== +# generate_batch() +# =================================================================== + + +async def test_generate_batch_order_preservation(client): + emb0 = [1.0, 2.0] + emb1 = [3.0, 4.0] + emb2 = [5.0, 6.0] + # Return in scrambled order to test sorting by index + data = [] + for idx, emb in [(2, emb2), (0, emb0), (1, emb1)]: + item = MagicMock() + item.embedding = emb + item.index = idx + data.append(item) + resp = MagicMock() + resp.data = data + + mock_openai = MagicMock() + mock_openai.embeddings.create = AsyncMock(return_value=resp) + client._client = mock_openai + + result = await client.generate_batch(["a", "b", "c"]) + assert result == [emb0, emb1, emb2] + + +async def test_generate_batch_empty(client): + result = await client.generate_batch([]) + assert result == [] + + +async def test_generate_batch_api_failure(client): + mock_openai = MagicMock() + mock_openai.embeddings.create = AsyncMock(side_effect=Exception("timeout")) + client._client = mock_openai + + with pytest.raises(EmbeddingError, match="timeout"): + await client.generate_batch(["hello"]) + + +# =================================================================== +# close() +# =================================================================== + + +async def test_close(client): + mock_openai = AsyncMock() + client._client = mock_openai + await client.close() + mock_openai.close.assert_awaited_once() + assert client._client is None + + +async def test_close_noop_when_no_client(): + client = AsyncEmbeddingsClient(endpoint="https://x.openai.azure.com/") + await client.close() # should not raise + + +# =================================================================== +# async context manager +# =================================================================== + + +async def test_context_manager(): + mock_openai = AsyncMock() + client = AsyncEmbeddingsClient( + endpoint="https://fake.openai.azure.com/", api_key="k" + ) + client._client = mock_openai + + async with client as c: + assert c is client + mock_openai.close.assert_awaited_once() diff --git a/tests/unit/aio/test_memory.py b/tests/unit/aio/test_memory.py new file mode 100644 index 0000000..8f206d5 --- /dev/null +++ b/tests/unit/aio/test_memory.py @@ -0,0 +1,360 @@ +"""Unit tests for AsyncAgentMemory.""" + +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from agent_memory_toolkit.aio.memory import AsyncAgentMemory +from agent_memory_toolkit.exceptions import CosmosNotConnectedError, MemoryNotFoundError, ValidationError +from agent_memory_toolkit.models import MemoryRecord + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +class AsyncIterator: + def __init__(self, items): + self._items = iter(items) + + def __aiter__(self): + return self + + async def __anext__(self): + try: + return next(self._items) + except StopIteration: + raise StopAsyncIteration + + +@pytest.fixture +def memory(): + """AsyncAgentMemory with default credential disabled (no azure.identity import).""" + return AsyncAgentMemory(use_default_credential=False) + + +@pytest.fixture +def memory_with_cosmos(memory): + """AsyncAgentMemory with a mocked cosmos store already attached.""" + mock_store = AsyncMock() + # _require_connected is a sync method, so override with a plain MagicMock + mock_store._require_connected = MagicMock() + memory._cosmos_store = mock_store + return memory + + +# =================================================================== +# Constructor +# =================================================================== + + +async def test_constructor_default_credential_disabled(): + mem = AsyncAgentMemory(use_default_credential=False) + assert mem.local_memory == [] + assert mem._cosmos_store is None + + +async def test_constructor_default_credential_enabled(): + mock_cred = MagicMock() + with patch( + "agent_memory_toolkit.aio.memory.DefaultAzureCredential", + return_value=mock_cred, + create=True, + ): + mem = AsyncAgentMemory(use_default_credential=True) + assert mem._cosmos_credential is not None + + +# =================================================================== +# add_local() +# =================================================================== + + +async def test_add_local_valid(memory): + memory.add_local(user_id="u1", role="user", content="hi") + assert len(memory.local_memory) == 1 + m = memory.local_memory[0] + assert m["user_id"] == "u1" + assert m["role"] == "user" + assert m["content"] == "hi" + assert m["type"] == "turn" + + +async def test_add_local_all_fields(memory): + memory.add_local( + user_id="u1", + role="agent", + content="response", + memory_type="summary", + agent_id="bot-1", + metadata={"k": "v"}, + thread_id="t-custom", + ) + m = memory.local_memory[0] + assert m["role"] == "agent" + assert m["type"] == "summary" + assert m["agent_id"] == "bot-1" + assert m["metadata"] == {"k": "v"} + assert m["thread_id"] == "t-custom" + + +async def test_add_local_invalid_role(memory): + with pytest.raises(ValidationError, match="role must be one of"): + memory.add_local(user_id="u1", role="invalid", content="hi") + + +async def test_add_local_invalid_type(memory): + with pytest.raises(ValidationError, match="type must be one of"): + memory.add_local(user_id="u1", role="user", content="hi", memory_type="bad") + + +# =================================================================== +# get_local() +# =================================================================== + + +async def test_get_local_no_filter(memory): + memory.add_local(user_id="u1", role="user", content="a") + memory.add_local(user_id="u2", role="agent", content="b") + results = memory.get_local() + assert len(results) == 2 + + +async def test_get_local_with_filters(memory): + memory.add_local(user_id="u1", role="user", content="a") + memory.add_local(user_id="u1", role="agent", content="b") + memory.add_local(user_id="u2", role="user", content="c") + + results = memory.get_local(user_id="u1", role="user") + assert len(results) == 1 + assert results[0]["content"] == "a" + + +async def test_get_local_by_id(memory): + memory.add_local(user_id="u1", role="user", content="x") + mid = memory.local_memory[0]["id"] + results = memory.get_local(memory_id=mid) + assert len(results) == 1 + assert results[0]["id"] == mid + + +async def test_get_local_by_type(memory): + memory.add_local(user_id="u1", role="user", content="a", memory_type="turn") + memory.add_local(user_id="u1", role="agent", content="b", memory_type="summary") + results = memory.get_local(memory_type="summary") + assert len(results) == 1 + + +# =================================================================== +# update_local() +# =================================================================== + + +async def test_update_local_success(memory): + memory.add_local(user_id="u1", role="user", content="old") + mid = memory.local_memory[0]["id"] + memory.update_local(memory_id=mid, content="new") + assert memory.local_memory[0]["content"] == "new" + assert "updated_at" in memory.local_memory[0] + + +async def test_update_local_not_found(memory): + with pytest.raises(MemoryNotFoundError): + memory.update_local(memory_id="nonexistent", content="x") + + +async def test_update_local_partial(memory): + memory.add_local(user_id="u1", role="user", content="old", memory_type="turn") + mid = memory.local_memory[0]["id"] + memory.update_local(memory_id=mid, metadata={"k": "v"}) + m = memory.local_memory[0] + assert m["content"] == "old" # unchanged + assert m["metadata"] == {"k": "v"} + + +# =================================================================== +# delete_local() +# =================================================================== + + +async def test_delete_local_success(memory): + memory.add_local(user_id="u1", role="user", content="a") + memory.add_local(user_id="u1", role="agent", content="b") + mid = memory.local_memory[0]["id"] + memory.delete_local(mid) + assert len(memory.local_memory) == 1 + assert memory.local_memory[0]["content"] == "b" + + +async def test_delete_local_not_found(memory): + with pytest.raises(MemoryNotFoundError): + memory.delete_local("nonexistent") + + +# =================================================================== +# connect_cosmos() +# =================================================================== + + +async def test_connect_cosmos(): + mem = AsyncAgentMemory( + cosmos_endpoint="https://fake.documents.azure.com:443/", + cosmos_credential=MagicMock(), + use_default_credential=False, + ) + mock_store_cls = MagicMock() + mock_store_instance = AsyncMock() + mock_store_cls.return_value = mock_store_instance + + with patch( + "agent_memory_toolkit.aio.memory.AsyncCosmosMemoryStore", mock_store_cls + ): + await mem.connect_cosmos() + + mock_store_instance.connect.assert_awaited_once() + assert mem._cosmos_store is mock_store_instance + + +# =================================================================== +# add_cosmos() +# =================================================================== + + +async def test_add_cosmos(memory_with_cosmos): + await memory_with_cosmos.add_cosmos( + user_id="u1", role="user", content="hello" + ) + memory_with_cosmos._cosmos_store.upsert.assert_awaited_once() + record = memory_with_cosmos._cosmos_store.upsert.call_args.kwargs.get("record") + if record is None: + record = memory_with_cosmos._cosmos_store.upsert.call_args.args[0] + assert isinstance(record, MemoryRecord) + assert record.content == "hello" + + +async def test_add_cosmos_not_connected(memory): + with pytest.raises(CosmosNotConnectedError): + await memory.add_cosmos(user_id="u1", role="user", content="hi") + + +# =================================================================== +# push_to_cosmos() +# =================================================================== + + +async def test_push_to_cosmos(memory_with_cosmos): + memory_with_cosmos.add_local(user_id="u1", role="user", content="a") + memory_with_cosmos.add_local(user_id="u1", role="agent", content="b") + + await memory_with_cosmos.push_to_cosmos(batch_size=5) + memory_with_cosmos._cosmos_store.upsert_batch.assert_awaited_once() + call_args = memory_with_cosmos._cosmos_store.upsert_batch.call_args + records = call_args.args[0] if call_args.args else call_args.kwargs["records"] + assert len(records) == 2 + + +async def test_push_to_cosmos_not_connected(memory): + memory.add_local(user_id="u1", role="user", content="a") + with pytest.raises(CosmosNotConnectedError): + await memory.push_to_cosmos() + + +async def test_push_to_cosmos_invalid_batch_size(memory_with_cosmos): + with pytest.raises(ValueError, match="batch_size must be greater than 0"): + await memory_with_cosmos.push_to_cosmos(batch_size=0) + + +# =================================================================== +# search_cosmos() +# =================================================================== + + +async def test_search_cosmos(memory_with_cosmos): + memory_with_cosmos._embeddings_client = AsyncMock() + memory_with_cosmos._embeddings_client.generate = AsyncMock( + return_value=[0.1, 0.2, 0.3] + ) + memory_with_cosmos._cosmos_store.vector_search = AsyncMock( + return_value=[{"id": "m1", "content": "result"}] + ) + + results = await memory_with_cosmos.search_cosmos( + search_terms="weather", user_id="u1", top_k=3 + ) + assert len(results) == 1 + memory_with_cosmos._embeddings_client.generate.assert_awaited_once_with("weather") + memory_with_cosmos._cosmos_store.vector_search.assert_awaited_once() + + +async def test_search_cosmos_not_connected(memory): + with pytest.raises(CosmosNotConnectedError): + await memory.search_cosmos(search_terms="test") + + +# =================================================================== +# get_memories() / get_thread() +# =================================================================== + + +async def test_get_memories_delegates(memory_with_cosmos): + memory_with_cosmos._cosmos_store.get_memories = AsyncMock(return_value=[{"id": "x"}]) + result = await memory_with_cosmos.get_memories(user_id="u1") + assert len(result) == 1 + memory_with_cosmos._cosmos_store.get_memories.assert_awaited_once() + + +async def test_get_thread_delegates(memory_with_cosmos): + memory_with_cosmos._cosmos_store.get_thread = AsyncMock(return_value=[{"id": "x"}]) + result = await memory_with_cosmos.get_thread(thread_id="t1") + assert len(result) == 1 + + +# =================================================================== +# Cosmos ops without connect +# =================================================================== + + +async def test_cosmos_ops_without_connect(memory): + with pytest.raises(CosmosNotConnectedError): + await memory.get_memories() + with pytest.raises(CosmosNotConnectedError): + await memory.get_thread(thread_id="t1") + with pytest.raises(CosmosNotConnectedError): + await memory.update_cosmos(memory_id="m1") + with pytest.raises(CosmosNotConnectedError): + await memory.delete_cosmos(memory_id="m1", thread_id="t1", user_id="u1") + + +# =================================================================== +# close() +# =================================================================== + + +async def test_close(memory_with_cosmos): + memory_with_cosmos._embeddings_client = AsyncMock() + memory_with_cosmos._processing_client = AsyncMock() + + await memory_with_cosmos.close() + memory_with_cosmos._cosmos_store.close.assert_awaited_once() + memory_with_cosmos._embeddings_client.close.assert_awaited_once() + memory_with_cosmos._processing_client.close.assert_awaited_once() + + +async def test_close_without_cosmos(memory): + memory._embeddings_client = AsyncMock() + memory._processing_client = AsyncMock() + await memory.close() # should not raise + + +# =================================================================== +# async context manager +# =================================================================== + + +async def test_context_manager(memory_with_cosmos): + memory_with_cosmos._embeddings_client = AsyncMock() + memory_with_cosmos._processing_client = AsyncMock() + + async with memory_with_cosmos as m: + assert m is memory_with_cosmos + memory_with_cosmos._cosmos_store.close.assert_awaited_once() diff --git a/tests/unit/aio/test_processing.py b/tests/unit/aio/test_processing.py new file mode 100644 index 0000000..29d8cde --- /dev/null +++ b/tests/unit/aio/test_processing.py @@ -0,0 +1,287 @@ +"""Unit tests for AsyncProcessingClient.""" + +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from agent_memory_toolkit.aio.processing import AsyncProcessingClient +from agent_memory_toolkit.exceptions import ( + ConfigurationError, + OrchestrationTimeoutError, + ProcessingError, +) + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +class MockAsyncContextManager: + """Simulates an aiohttp response context manager.""" + + def __init__(self, return_value): + self._return_value = return_value + + async def __aenter__(self): + return self._return_value + + async def __aexit__(self, *args): + pass + + +def _mock_response(json_data: dict): + resp = MagicMock() + resp.json = AsyncMock(return_value=json_data) + return resp + + +@pytest.fixture +def client(): + return AsyncProcessingClient( + endpoint="https://my-func.azurewebsites.net", + key="test-key", + poll_interval=0.01, + timeout=1.0, + ) + + +@pytest.fixture +def client_with_session(client): + """Client with a pre-attached mock aiohttp session.""" + session = MagicMock() + session.closed = False + session.close = AsyncMock() + client._session = session + return client, session + + +# =================================================================== +# invoke_orchestrator() — immediate completion +# =================================================================== + + +async def test_invoke_immediate_completion(client_with_session): + client, session = client_with_session + + start_resp = _mock_response({"runtimeStatus": "Completed", "output": "done"}) + session.post = MagicMock(return_value=MockAsyncContextManager(start_resp)) + + result = await client.invoke_orchestrator({"user_id": "u1"}) + assert result["runtimeStatus"] == "Completed" + + +# =================================================================== +# invoke_orchestrator() — polls multiple times +# =================================================================== + + +async def test_invoke_polls_until_completed(client_with_session): + client, session = client_with_session + + start_resp = _mock_response({ + "id": "inst-1", + "statusQueryGetUri": "https://status/inst-1", + }) + poll_running = _mock_response({"runtimeStatus": "Running"}) + poll_completed = _mock_response({"runtimeStatus": "Completed", "output": "ok"}) + + session.post = MagicMock(return_value=MockAsyncContextManager(start_resp)) + session.get = MagicMock( + side_effect=[ + MockAsyncContextManager(poll_running), + MockAsyncContextManager(poll_completed), + ] + ) + + result = await client.invoke_orchestrator({"data": 1}) + assert result["runtimeStatus"] == "Completed" + assert session.get.call_count == 2 + + +# =================================================================== +# invoke_orchestrator() — Failed +# =================================================================== + + +async def test_invoke_failed(client_with_session): + client, session = client_with_session + + start_resp = _mock_response({ + "id": "inst-2", + "statusQueryGetUri": "https://status/inst-2", + }) + poll_failed = _mock_response({ + "runtimeStatus": "Failed", + "output": "something went wrong", + }) + + session.post = MagicMock(return_value=MockAsyncContextManager(start_resp)) + session.get = MagicMock( + return_value=MockAsyncContextManager(poll_failed), + ) + + with pytest.raises(ProcessingError, match="something went wrong"): + await client.invoke_orchestrator({"data": 1}) + + +# =================================================================== +# invoke_orchestrator() — timeout +# =================================================================== + + +async def test_invoke_timeout(): + client = AsyncProcessingClient( + endpoint="https://my-func.azurewebsites.net", + key="k", + poll_interval=0.01, + timeout=0.05, + ) + session = MagicMock() + session.closed = False + session.close = AsyncMock() + client._session = session + + start_resp = _mock_response({ + "id": "inst-3", + "statusQueryGetUri": "https://status/inst-3", + }) + poll_running = _mock_response({"runtimeStatus": "Running"}) + + session.post = MagicMock(return_value=MockAsyncContextManager(start_resp)) + session.get = MagicMock(return_value=MockAsyncContextManager(poll_running)) + + with pytest.raises(OrchestrationTimeoutError): + await client.invoke_orchestrator({"data": 1}) + + +# =================================================================== +# invoke_orchestrator() — missing endpoint +# =================================================================== + + +async def test_invoke_missing_endpoint(): + client = AsyncProcessingClient(endpoint=None, key="k") + with pytest.raises(ConfigurationError): + await client.invoke_orchestrator({"data": 1}) + + +# =================================================================== +# generate_thread_summary() — payload correct +# =================================================================== + + +async def test_generate_thread_summary_payload(client_with_session): + client, session = client_with_session + + start_resp = _mock_response({"runtimeStatus": "Completed", "output": "summary"}) + session.post = MagicMock(return_value=MockAsyncContextManager(start_resp)) + + result = await client.generate_thread_summary( + user_id="u1", thread_id="t1", recent_k=5 + ) + assert result["runtimeStatus"] == "Completed" + + # Check the JSON payload sent to session.post + call_kwargs = session.post.call_args.kwargs + payload = call_kwargs["json"] + assert payload["user_id"] == "u1" + assert payload["thread_id"] == "t1" + assert payload["thread_summary_only"] is True + assert payload["recent_k"] == 5 + + +async def test_generate_thread_summary_no_recent_k(client_with_session): + client, session = client_with_session + start_resp = _mock_response({"runtimeStatus": "Completed"}) + session.post = MagicMock(return_value=MockAsyncContextManager(start_resp)) + + await client.generate_thread_summary(user_id="u1", thread_id="t1") + payload = session.post.call_args.kwargs["json"] + assert "recent_k" not in payload + + +# =================================================================== +# extract_facts() +# =================================================================== + + +async def test_extract_facts_payload(client_with_session): + client, session = client_with_session + start_resp = _mock_response({"runtimeStatus": "Completed"}) + session.post = MagicMock(return_value=MockAsyncContextManager(start_resp)) + + await client.extract_facts(user_id="u1", thread_id="t1") + payload = session.post.call_args.kwargs["json"] + assert payload["extract_facts_only"] is True + + +# =================================================================== +# close() +# =================================================================== + + +async def test_close(client_with_session): + client, session = client_with_session + await client.close() + session.close.assert_awaited_once() + assert client._session is None + + +async def test_close_noop_when_no_session(): + client = AsyncProcessingClient(endpoint="https://x.azurewebsites.net") + await client.close() # should not raise + + +# =================================================================== +# async context manager +# =================================================================== + + +async def test_context_manager(): + client = AsyncProcessingClient( + endpoint="https://func.azurewebsites.net", key="k" + ) + session = MagicMock() + session.closed = False + session.close = AsyncMock() + client._session = session + + async with client as c: + assert c is client + session.close.assert_awaited_once() + + +# =================================================================== +# URL construction +# =================================================================== + + +async def test_url_includes_key(client_with_session): + client, session = client_with_session + start_resp = _mock_response({"runtimeStatus": "Completed"}) + session.post = MagicMock(return_value=MockAsyncContextManager(start_resp)) + + await client.invoke_orchestrator({"data": 1}) + url = session.post.call_args.args[0] + assert "?code=test-key" in url + assert "/orchestrators/memory_orchestrator" in url + + +async def test_url_no_key(): + client = AsyncProcessingClient( + endpoint="https://func.azurewebsites.net", + key=None, + poll_interval=0.01, + timeout=1.0, + ) + session = MagicMock() + session.closed = False + session.close = AsyncMock() + client._session = session + + start_resp = _mock_response({"runtimeStatus": "Completed"}) + session.post = MagicMock(return_value=MockAsyncContextManager(start_resp)) + + await client.invoke_orchestrator({"data": 1}) + url = session.post.call_args.args[0] + assert "?code=" not in url diff --git a/tests/unit/test_cosmos_memory_client.py b/tests/unit/test_cosmos_memory_client.py new file mode 100644 index 0000000..9d9eab6 --- /dev/null +++ b/tests/unit/test_cosmos_memory_client.py @@ -0,0 +1,385 @@ +"""Unit tests for CosmosMemoryStore (sync Cosmos DB client).""" + +from __future__ import annotations + +import uuid +from unittest.mock import MagicMock, patch + +import pytest + +from agent_memory_toolkit.cosmos_memory_client import CosmosMemoryStore +from agent_memory_toolkit.exceptions import ( + ConfigurationError, + CosmosNotConnectedError, + CosmosOperationError, + MemoryNotFoundError, +) +from agent_memory_toolkit.models import MemoryRecord + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _make_store( + endpoint: str | None = "https://fake.documents.azure.com", + credential: str | None = "fake-key", + database: str = "ai_memory", + container: str = "memories", +) -> CosmosMemoryStore: + return CosmosMemoryStore( + endpoint=endpoint, + credential=credential, + database=database, + container=container, + ) + + +def _connected_store() -> tuple[CosmosMemoryStore, MagicMock]: + """Return a store with a mocked container client already wired up.""" + store = _make_store() + container = MagicMock() + store._container_client = container + return store, container + + +def _make_record(**overrides) -> MemoryRecord: + defaults = dict( + id=str(uuid.uuid4()), + user_id="u1", + thread_id="t1", + role="user", + content="hello", + ) + defaults.update(overrides) + return MemoryRecord(**defaults) + + +# --------------------------------------------------------------------------- +# connect() +# --------------------------------------------------------------------------- + + +class TestConnect: + @patch("azure.cosmos.CosmosClient") + def test_connect_success(self, MockCosmosClient): + mock_client = MagicMock() + mock_db = MagicMock() + mock_container = MagicMock() + MockCosmosClient.return_value = mock_client + mock_client.get_database_client.return_value = mock_db + mock_db.get_container_client.return_value = mock_container + + store = _make_store() + store.connect() + + MockCosmosClient.assert_called_once_with( + "https://fake.documents.azure.com", credential="fake-key" + ) + mock_client.get_database_client.assert_called_once_with("ai_memory") + mock_db.get_container_client.assert_called_once_with("memories") + assert store._container_client is mock_container + + def test_connect_missing_endpoint(self): + store = _make_store(endpoint=None) + with pytest.raises(ConfigurationError) as exc_info: + store.connect() + assert exc_info.value.parameter == "endpoint" + + def test_connect_missing_credential(self): + store = _make_store(credential=None) + with pytest.raises(ConfigurationError) as exc_info: + store.connect() + assert exc_info.value.parameter == "credential" + + +# --------------------------------------------------------------------------- +# _require_connected() +# --------------------------------------------------------------------------- + + +class TestRequireConnected: + def test_raises_when_not_connected(self): + store = _make_store() + with pytest.raises(CosmosNotConnectedError): + store._require_connected() + + +# --------------------------------------------------------------------------- +# create_store() +# --------------------------------------------------------------------------- + + +class TestCreateStore: + @patch("azure.cosmos.CosmosClient") + def test_create_store_success(self, MockCosmosClient): + mock_client = MagicMock() + mock_db = MagicMock() + mock_container = MagicMock() + MockCosmosClient.return_value = mock_client + mock_client.create_database_if_not_exists.return_value = mock_db + mock_db.create_container_if_not_exists.return_value = mock_container + + store = _make_store() + store.create_store(embedding_dimensions=256) + + mock_client.create_database_if_not_exists.assert_called_once_with( + id="ai_memory" + ) + call_kwargs = mock_db.create_container_if_not_exists.call_args + assert call_kwargs.kwargs["id"] == "memories" + + # Verify vector embedding policy includes correct dimensions + vec_policy = call_kwargs.kwargs["vector_embedding_policy"] + assert vec_policy["vectorEmbeddings"][0]["dimensions"] == 256 + + # Verify full-text policy + ft_policy = call_kwargs.kwargs["full_text_policy"] + assert ft_policy["defaultLanguage"] == "en-US" + + assert store._container_client is mock_container + + +# --------------------------------------------------------------------------- +# upsert / upsert_batch +# --------------------------------------------------------------------------- + + +class TestUpsert: + def test_upsert_success(self): + store, container = _connected_store() + rec = _make_record() + store.upsert(rec) + container.upsert_item.assert_called_once() + body = container.upsert_item.call_args.kwargs["body"] + assert body["id"] == rec.id + assert body["content"] == "hello" + + def test_upsert_not_connected(self): + store = _make_store() + with pytest.raises(CosmosNotConnectedError): + store.upsert(_make_record()) + + def test_upsert_cosmos_error(self): + store, container = _connected_store() + container.upsert_item.side_effect = RuntimeError("boom") + with pytest.raises(CosmosOperationError): + store.upsert(_make_record()) + + def test_upsert_batch(self): + store, container = _connected_store() + records = [_make_record(id=f"r{i}") for i in range(3)] + store.upsert_batch(records) + assert container.upsert_item.call_count == 3 + + +# --------------------------------------------------------------------------- +# get_memories() +# --------------------------------------------------------------------------- + + +class TestGetMemories: + def test_no_filters(self, sample_memory_dict): + store, container = _connected_store() + container.query_items.return_value = [sample_memory_dict] + + result = store.get_memories() + + call_kwargs = container.query_items.call_args.kwargs + assert "WHERE" not in call_kwargs["query"] + assert result == [sample_memory_dict] + + def test_all_filters(self, sample_memory_dict): + store, container = _connected_store() + container.query_items.return_value = [sample_memory_dict] + + store.get_memories( + memory_id="m1", + user_id="u1", + thread_id="t1", + role="user", + memory_type="turn", + ) + + call_kwargs = container.query_items.call_args.kwargs + query = call_kwargs["query"] + assert "WHERE" in query + params = call_kwargs["parameters"] + param_names = {p["name"] for p in params} + assert "@memory_id" in param_names + assert "@user_id" in param_names + assert "@thread_id" in param_names + assert "@role" in param_names + assert "@memory_type" in param_names + + def test_recent_k(self, sample_memory_dict): + store, container = _connected_store() + # Simulate docs returned in DESC order + doc1 = {**sample_memory_dict, "id": "old"} + doc2 = {**sample_memory_dict, "id": "new"} + container.query_items.return_value = [doc2, doc1] + + result = store.get_memories(recent_k=2) + + call_kwargs = container.query_items.call_args.kwargs + query = call_kwargs["query"] + assert "TOP @recent_k" in query + assert "ORDER BY c._ts DESC" in query + # Result should be reversed to chronological + assert result[0]["id"] == "old" + assert result[1]["id"] == "new" + + +# --------------------------------------------------------------------------- +# get_thread() +# --------------------------------------------------------------------------- + + +class TestGetThread: + def test_basic(self, sample_memory_dicts): + store, container = _connected_store() + container.query_items.return_value = list(reversed(sample_memory_dicts)) + + result = store.get_thread(thread_id="t1") + + call_kwargs = container.query_items.call_args.kwargs + assert "@thread_id" in call_kwargs["query"] or any( + p["name"] == "@thread_id" for p in call_kwargs["parameters"] + ) + # Should be reversed to chronological + assert len(result) == 3 + + def test_with_recent_k(self, sample_memory_dicts): + store, container = _connected_store() + # Return 3 docs (DESC order), get_thread truncates + reverses + container.query_items.return_value = list(reversed(sample_memory_dicts)) + + result = store.get_thread(thread_id="t1", recent_k=2) + assert len(result) == 2 + + +# --------------------------------------------------------------------------- +# update() +# --------------------------------------------------------------------------- + + +class TestUpdate: + def test_success(self, sample_memory_dict): + store, container = _connected_store() + container.query_items.return_value = [sample_memory_dict.copy()] + + store.update(memory_id=sample_memory_dict["id"], content="updated") + + container.replace_item.assert_called_once() + body = container.replace_item.call_args.kwargs["body"] + assert body["content"] == "updated" + assert "updated_at" in body + + def test_not_found(self): + store, container = _connected_store() + container.query_items.return_value = [] + + with pytest.raises(MemoryNotFoundError): + store.update(memory_id="no-such-id", content="x") + + +# --------------------------------------------------------------------------- +# delete() +# --------------------------------------------------------------------------- + + +class TestDelete: + def test_success(self, sample_memory_dict): + store, container = _connected_store() + container.query_items.return_value = [sample_memory_dict] + + store.delete( + memory_id=sample_memory_dict["id"], + user_id=sample_memory_dict["user_id"], + thread_id=sample_memory_dict["thread_id"], + ) + + container.delete_item.assert_called_once_with( + item=sample_memory_dict["id"], + partition_key=[ + sample_memory_dict["user_id"], + sample_memory_dict["thread_id"], + ], + ) + + def test_not_found(self): + store, container = _connected_store() + container.query_items.return_value = [] + + with pytest.raises(MemoryNotFoundError): + store.delete(memory_id="nope", user_id="u1", thread_id="t1") + + +# --------------------------------------------------------------------------- +# vector_search() +# --------------------------------------------------------------------------- + + +class TestVectorSearch: + def test_vector_only(self, sample_embedding, sample_memory_dict): + store, container = _connected_store() + container.query_items.return_value = [sample_memory_dict] + + result = store.vector_search(query_vector=sample_embedding, top_k=3) + + call_kwargs = container.query_items.call_args.kwargs + query = call_kwargs["query"] + assert "VectorDistance" in query + assert "RRF" not in query + assert result == [sample_memory_dict] + + def test_hybrid(self, sample_embedding, sample_memory_dict): + store, container = _connected_store() + container.query_items.return_value = [sample_memory_dict] + + store.vector_search( + query_vector=sample_embedding, + hybrid_search=True, + search_terms="weather Seattle", + top_k=5, + ) + + call_kwargs = container.query_items.call_args.kwargs + query = call_kwargs["query"] + assert "RANK RRF" in query + assert "FullTextScore" in query + params = call_kwargs["parameters"] + key_terms_param = [p for p in params if p["name"] == "@key_terms"] + assert key_terms_param[0]["value"] == "weather Seattle" + + def test_with_filters(self, sample_embedding, sample_memory_dict): + store, container = _connected_store() + container.query_items.return_value = [sample_memory_dict] + + store.vector_search( + query_vector=sample_embedding, + user_id="u1", + role="user", + ) + + call_kwargs = container.query_items.call_args.kwargs + query = call_kwargs["query"] + assert "WHERE" in query + + +# --------------------------------------------------------------------------- +# get_user_summary() +# --------------------------------------------------------------------------- + + +class TestGetUserSummary: + def test_filters_by_type(self, sample_memory_dict): + store, container = _connected_store() + summary_doc = {**sample_memory_dict, "type": "user_summary"} + container.query_items.return_value = [summary_doc] + + result = store.get_user_summary(user_id="u1") + + call_kwargs = container.query_items.call_args.kwargs + query = call_kwargs["query"] + assert "user_summary" in query + assert result == [summary_doc] diff --git a/tests/unit/test_embeddings.py b/tests/unit/test_embeddings.py new file mode 100644 index 0000000..f68deaf --- /dev/null +++ b/tests/unit/test_embeddings.py @@ -0,0 +1,203 @@ +"""Unit tests for EmbeddingsClient (sync embedding client).""" + +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +import pytest + +from agent_memory_toolkit.embeddings import EmbeddingsClient +from agent_memory_toolkit.exceptions import ConfigurationError, EmbeddingError + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _make_client(**overrides) -> EmbeddingsClient: + defaults = dict( + endpoint="https://fake.openai.azure.com", + api_key="sk-fake", + model="text-embedding-3-large", + ) + defaults.update(overrides) + return EmbeddingsClient(**defaults) + + +def _mock_embedding_response(embedding: list[float]): + """Build a mock response for a single embedding call.""" + item = MagicMock() + item.embedding = embedding + item.index = 0 + resp = MagicMock() + resp.data = [item] + return resp + + +def _mock_batch_response(embeddings: list[list[float]]): + """Build a mock response for a batch embedding call with index fields.""" + items = [] + for i, emb in enumerate(embeddings): + item = MagicMock() + item.embedding = emb + item.index = i + items.append(item) + resp = MagicMock() + resp.data = items + return resp + + +# --------------------------------------------------------------------------- +# generate() +# --------------------------------------------------------------------------- + + +class TestGenerate: + @patch("openai.AzureOpenAI") + def test_success(self, MockAOAI, sample_embedding): + mock_client = MagicMock() + MockAOAI.return_value = mock_client + mock_client.embeddings.create.return_value = _mock_embedding_response( + sample_embedding + ) + + client = _make_client() + result = client.generate("hello world") + + assert result == sample_embedding + mock_client.embeddings.create.assert_called_once() + + @patch("openai.AzureOpenAI") + def test_lazy_init_reuses_client(self, MockAOAI, sample_embedding): + mock_client = MagicMock() + MockAOAI.return_value = mock_client + mock_client.embeddings.create.return_value = _mock_embedding_response( + sample_embedding + ) + + client = _make_client() + client.generate("first") + client.generate("second") + + # AzureOpenAI constructor should be called only once + MockAOAI.assert_called_once() + assert mock_client.embeddings.create.call_count == 2 + + @patch("openai.AzureOpenAI") + def test_with_api_key(self, MockAOAI, sample_embedding): + mock_client = MagicMock() + MockAOAI.return_value = mock_client + mock_client.embeddings.create.return_value = _mock_embedding_response( + sample_embedding + ) + + client = _make_client(api_key="my-key", credential=None) + client.generate("text") + + call_kwargs = MockAOAI.call_args.kwargs + assert call_kwargs["api_key"] == "my-key" + + @patch("azure.identity.get_bearer_token_provider") + @patch("openai.AzureOpenAI") + def test_with_credential( + self, MockAOAI, mock_get_token, sample_embedding, mock_credential + ): + mock_client = MagicMock() + MockAOAI.return_value = mock_client + mock_client.embeddings.create.return_value = _mock_embedding_response( + sample_embedding + ) + mock_get_token.return_value = lambda: "token" + + client = _make_client(api_key=None, credential=mock_credential) + client.generate("text") + + mock_get_token.assert_called_once() + call_kwargs = MockAOAI.call_args.kwargs + assert "azure_ad_token_provider" in call_kwargs + + def test_missing_endpoint(self): + client = EmbeddingsClient(endpoint=None, api_key="key") + with pytest.raises(ConfigurationError) as exc_info: + client.generate("text") + assert exc_info.value.parameter == "endpoint" + + def test_missing_key_and_credential(self): + client = EmbeddingsClient( + endpoint="https://fake.openai.azure.com", + api_key=None, + credential=None, + ) + with pytest.raises(ConfigurationError) as exc_info: + client.generate("text") + assert exc_info.value.parameter == "credential" + + @patch("openai.AzureOpenAI") + def test_api_failure(self, MockAOAI): + mock_client = MagicMock() + MockAOAI.return_value = mock_client + mock_client.embeddings.create.side_effect = RuntimeError("API down") + + client = _make_client() + with pytest.raises(EmbeddingError) as exc_info: + client.generate("text") + assert "API down" in str(exc_info.value) + + @patch("openai.AzureOpenAI") + def test_with_dimensions(self, MockAOAI, sample_embedding): + mock_client = MagicMock() + MockAOAI.return_value = mock_client + mock_client.embeddings.create.return_value = _mock_embedding_response( + sample_embedding + ) + + client = _make_client(dimensions=256) + client.generate("text") + + call_kwargs = mock_client.embeddings.create.call_args.kwargs + assert call_kwargs["dimensions"] == 256 + + +# --------------------------------------------------------------------------- +# generate_batch() +# --------------------------------------------------------------------------- + + +class TestGenerateBatch: + @patch("openai.AzureOpenAI") + def test_preserves_order(self, MockAOAI): + mock_client = MagicMock() + MockAOAI.return_value = mock_client + + emb_a = [0.1, 0.2] + emb_b = [0.3, 0.4] + + # Return items out of order to verify sorting by index + item_b = MagicMock() + item_b.embedding = emb_b + item_b.index = 1 + item_a = MagicMock() + item_a.embedding = emb_a + item_a.index = 0 + resp = MagicMock() + resp.data = [item_b, item_a] # intentionally reversed + mock_client.embeddings.create.return_value = resp + + client = _make_client() + result = client.generate_batch(["text_a", "text_b"]) + + assert result == [emb_a, emb_b] + + def test_empty_list(self): + client = _make_client() + result = client.generate_batch([]) + assert result == [] + + @patch("openai.AzureOpenAI") + def test_batch_api_failure(self, MockAOAI): + mock_client = MagicMock() + MockAOAI.return_value = mock_client + mock_client.embeddings.create.side_effect = RuntimeError("batch fail") + + client = _make_client() + with pytest.raises(EmbeddingError): + client.generate_batch(["a", "b"]) diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py new file mode 100644 index 0000000..f3872c8 --- /dev/null +++ b/tests/unit/test_exceptions.py @@ -0,0 +1,111 @@ +"""Unit tests for agent_memory_toolkit.exceptions.""" + +import pytest + +from agent_memory_toolkit.exceptions import ( + AgentMemoryError, + AuthenticationError, + ConfigurationError, + CosmosNotConnectedError, + CosmosOperationError, + EmbeddingError, + MemoryNotFoundError, + OrchestrationTimeoutError, + ProcessingError, + ValidationError, +) + +# --------------------------------------------------------------------------- +# Inheritance +# --------------------------------------------------------------------------- + +ALL_SUBTYPES = [ + ConfigurationError, + ValidationError, + CosmosNotConnectedError, + CosmosOperationError, + MemoryNotFoundError, + EmbeddingError, + ProcessingError, + OrchestrationTimeoutError, + AuthenticationError, +] + + +@pytest.mark.parametrize("exc_cls", ALL_SUBTYPES, ids=lambda c: c.__name__) +def test_all_exceptions_inherit_from_agent_memory_error(exc_cls): + assert issubclass(exc_cls, AgentMemoryError) + + +def test_catch_all_subtypes(): + """try/except AgentMemoryError catches every subtype.""" + for cls in ALL_SUBTYPES: + with pytest.raises(AgentMemoryError): + raise cls("test") + + +# --------------------------------------------------------------------------- +# ConfigurationError +# --------------------------------------------------------------------------- + + +def test_configuration_error_parameter_kwarg(): + err = ConfigurationError(parameter="cosmos_endpoint") + assert err.parameter == "cosmos_endpoint" + assert "Missing or invalid configuration: cosmos_endpoint" in str(err) + + +def test_configuration_error_custom_message(): + err = ConfigurationError("custom msg", parameter="p") + assert str(err) == "custom msg" + assert err.parameter == "p" + + +# --------------------------------------------------------------------------- +# MemoryNotFoundError +# --------------------------------------------------------------------------- + + +def test_memory_not_found_full_context(): + err = MemoryNotFoundError(memory_id="m1", user_id="u1", thread_id="t1") + assert err.memory_id == "m1" + assert err.user_id == "u1" + assert err.thread_id == "t1" + msg = str(err) + assert "m1" in msg + assert "u1" in msg + assert "t1" in msg + + +def test_memory_not_found_partial_context(): + err = MemoryNotFoundError(memory_id="m2") + assert err.memory_id == "m2" + assert err.user_id is None + assert err.thread_id is None + msg = str(err) + assert "m2" in msg + assert "user_id" not in msg + + +# --------------------------------------------------------------------------- +# CosmosNotConnectedError +# --------------------------------------------------------------------------- + + +def test_cosmos_not_connected_default_message(): + err = CosmosNotConnectedError() + assert "connect_cosmos()" in str(err) + + +# --------------------------------------------------------------------------- +# OrchestrationTimeoutError +# --------------------------------------------------------------------------- + + +def test_orchestration_timeout_with_attrs(): + err = OrchestrationTimeoutError(timeout=30.0, status_url="https://example.com/status") + assert err.timeout == 30.0 + assert err.status_url == "https://example.com/status" + msg = str(err) + assert "30.0" in msg + assert "https://example.com/status" in msg diff --git a/tests/unit/test_memory.py b/tests/unit/test_memory.py new file mode 100644 index 0000000..12bed72 --- /dev/null +++ b/tests/unit/test_memory.py @@ -0,0 +1,287 @@ +"""Unit tests for the synchronous AgentMemory orchestrator.""" + +from unittest.mock import MagicMock, patch + +import pytest + +from agent_memory_toolkit.exceptions import CosmosNotConnectedError, MemoryNotFoundError, ValidationError +from agent_memory_toolkit.memory import AgentMemory +from agent_memory_toolkit.models import MemoryRecord + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _make_agent(**overrides) -> AgentMemory: + """Build an AgentMemory with credential auto-resolution disabled.""" + defaults = dict(use_default_credential=False) + defaults.update(overrides) + return AgentMemory(**defaults) + + +# =================================================================== +# Constructor +# =================================================================== + + +class TestConstructor: + """Tests 1-2: credential resolution.""" + + def test_default_credential_created_when_flag_true(self): + """use_default_credential=True creates a DefaultAzureCredential.""" + sentinel = MagicMock(name="default-cred") + mock_module = MagicMock() + mock_module.DefaultAzureCredential.return_value = sentinel + + with patch.dict("sys.modules", {"azure.identity": mock_module}): + mem = AgentMemory(use_default_credential=True) + mock_module.DefaultAzureCredential.assert_called_once() + assert mem._cosmos_credential is sentinel + assert mem._ai_foundry_credential is sentinel + + def test_no_credential_when_flag_false(self): + """use_default_credential=False leaves credentials as None.""" + mem = _make_agent() + assert mem._cosmos_credential is None + assert mem._ai_foundry_credential is None + + +# =================================================================== +# Local CRUD +# =================================================================== + + +class TestAddLocal: + """Tests 3-4: add_local.""" + + def test_add_local_valid(self): + mem = _make_agent() + mem.add_local(user_id="u1", role="user", content="hello") + + assert len(mem.local_memory) == 1 + m = mem.local_memory[0] + assert m["user_id"] == "u1" + assert m["role"] == "user" + assert m["content"] == "hello" + assert m["type"] == "turn" + assert "id" in m + assert "created_at" in m + + def test_add_local_invalid_role(self): + mem = _make_agent() + with pytest.raises(ValidationError, match="role must be one of"): + mem.add_local(user_id="u1", role="invalid", content="hi") + + +class TestGetLocal: + """Tests 5-6: get_local.""" + + def test_get_local_no_filters(self): + mem = _make_agent() + mem.add_local(user_id="u1", role="user", content="a") + mem.add_local(user_id="u2", role="agent", content="b") + + results = mem.get_local() + assert len(results) == 2 + + def test_get_local_with_filters(self): + mem = _make_agent() + mem.add_local(user_id="u1", role="user", content="a", memory_type="turn") + mem.add_local(user_id="u1", role="agent", content="b", memory_type="turn") + mem.add_local(user_id="u2", role="user", content="c", memory_type="summary") + + # AND logic: user_id=u1 AND role=user AND type=turn + results = mem.get_local(user_id="u1", role="user", memory_type="turn") + assert len(results) == 1 + assert results[0]["content"] == "a" + + +class TestUpdateLocal: + """Tests 7-8: update_local.""" + + def test_update_local_success(self): + mem = _make_agent() + mem.add_local(user_id="u1", role="user", content="old") + mid = mem.local_memory[0]["id"] + + mem.update_local(mid, content="new", metadata={"k": "v"}) + + m = mem.local_memory[0] + assert m["content"] == "new" + assert m["metadata"] == {"k": "v"} + assert "updated_at" in m + + def test_update_local_not_found(self): + mem = _make_agent() + with pytest.raises(MemoryNotFoundError): + mem.update_local("nonexistent-id", content="x") + + +class TestDeleteLocal: + """Tests 9-10: delete_local.""" + + def test_delete_local_success(self): + mem = _make_agent() + mem.add_local(user_id="u1", role="user", content="x") + mid = mem.local_memory[0]["id"] + + mem.delete_local(mid) + assert len(mem.local_memory) == 0 + + def test_delete_local_not_found(self): + mem = _make_agent() + with pytest.raises(MemoryNotFoundError): + mem.delete_local("nonexistent-id") + + +# =================================================================== +# Cosmos delegation +# =================================================================== + + +class TestConnectCosmos: + """Test 11: connect_cosmos.""" + + @patch("agent_memory_toolkit.memory.CosmosMemoryStore", autospec=True) + def test_connect_cosmos(self, mock_store_cls): + mem = _make_agent(cosmos_endpoint="https://test.documents.azure.com:443/") + mock_instance = mock_store_cls.return_value + + mem.connect_cosmos() + + mock_store_cls.assert_called_once_with( + endpoint="https://test.documents.azure.com:443/", + credential=None, + database="ai_memory", + container="memories", + ) + mock_instance.connect.assert_called_once() + assert mem._cosmos_store is mock_instance + + +class TestAddCosmos: + """Test 12: add_cosmos.""" + + def test_add_cosmos(self): + mem = _make_agent() + mock_store = MagicMock() + mem._cosmos_store = mock_store + + mem.add_cosmos(user_id="u1", role="user", content="hello") + + mock_store.upsert.assert_called_once() + record = mock_store.upsert.call_args[0][0] + assert isinstance(record, MemoryRecord) + assert record.user_id == "u1" + assert record.role == "user" + assert record.content == "hello" + + +class TestPushToCosmos: + """Test 13: push_to_cosmos.""" + + def test_push_to_cosmos(self): + mem = _make_agent() + mock_store = MagicMock() + mem._cosmos_store = mock_store + + mem.add_local(user_id="u1", role="user", content="a") + mem.add_local(user_id="u1", role="agent", content="b") + + mem.push_to_cosmos() + + mock_store.upsert_batch.assert_called_once() + records = mock_store.upsert_batch.call_args[0][0] + assert len(records) == 2 + assert all(isinstance(r, MemoryRecord) for r in records) + + +class TestGetMemories: + """Test 14: get_memories.""" + + def test_get_memories(self): + mem = _make_agent() + mock_store = MagicMock() + mock_store.get_memories.return_value = [{"id": "1", "content": "hi"}] + mem._cosmos_store = mock_store + + result = mem.get_memories(user_id="u1", role="user") + + mock_store.get_memories.assert_called_once_with( + memory_id=None, + user_id="u1", + thread_id=None, + role="user", + memory_type=None, + recent_k=None, + ) + assert result == [{"id": "1", "content": "hi"}] + + +class TestSearchCosmos: + """Test 15: search_cosmos.""" + + def test_search_cosmos(self): + mem = _make_agent() + mock_store = MagicMock() + mock_store.vector_search.return_value = [{"id": "1", "score": 0.95}] + mem._cosmos_store = mock_store + + mock_embed = MagicMock() + mock_embed.generate.return_value = [0.1, 0.2, 0.3] + mem._embeddings_client = mock_embed + + result = mem.search_cosmos(search_terms="weather", user_id="u1", top_k=3) + + mock_embed.generate.assert_called_once_with("weather") + mock_store.vector_search.assert_called_once_with( + query_vector=[0.1, 0.2, 0.3], + user_id="u1", + role=None, + memory_type=None, + thread_id=None, + hybrid_search=False, + search_terms="weather", + top_k=3, + ) + assert result == [{"id": "1", "score": 0.95}] + + +# =================================================================== +# Processing delegation +# =================================================================== + + +class TestGenerateThreadSummary: + """Test 16: generate_thread_summary.""" + + def test_generate_thread_summary(self): + mem = _make_agent() + mock_proc = MagicMock() + mock_proc.generate_thread_summary.return_value = {"status": "ok"} + mem._processing_client = mock_proc + + result = mem.generate_thread_summary(user_id="u1", thread_id="t1") + + mock_proc.generate_thread_summary.assert_called_once_with( + user_id="u1", + thread_id="t1", + recent_k=None, + poll_interval=2.0, + timeout=120.0, + ) + assert result == {"status": "ok"} + + +# =================================================================== +# Guard clause +# =================================================================== + + +class TestCosmosGuard: + """Test 17: Cosmos op without connect raises CosmosNotConnectedError.""" + + def test_get_memories_without_connect(self): + mem = _make_agent() + with pytest.raises(CosmosNotConnectedError): + mem.get_memories() diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py new file mode 100644 index 0000000..a1a2220 --- /dev/null +++ b/tests/unit/test_models.py @@ -0,0 +1,207 @@ +"""Unit tests for agent_memory_toolkit.models.""" + +import uuid + +import pydantic +import pytest + +from agent_memory_toolkit.models import ( + MemoryRecord, + OrchestrationResult, + SearchResult, +) + +# --------------------------------------------------------------------------- +# MemoryRecord – defaults +# --------------------------------------------------------------------------- + + +def test_memory_record_defaults(): + """Required fields only → id/thread_id are UUIDs, type=turn, metadata={}.""" + rec = MemoryRecord(user_id="u1", role="user", content="hello") + uuid.UUID(rec.id) # valid UUID + uuid.UUID(rec.thread_id) + assert rec.memory_type == "turn" + assert rec.metadata == {} + assert rec.embedding is None + assert rec.agent_id is None + assert rec.updated_at is None + + +def test_memory_record_all_fields(sample_user_id, sample_thread_id, sample_embedding): + """All fields populated are retained.""" + rec = MemoryRecord( + id="custom-id", + user_id=sample_user_id, + thread_id=sample_thread_id, + role="agent", + memory_type="summary", + content="summary content", + metadata={"key": "value"}, + embedding=sample_embedding, + agent_id="agent-1", + created_at="2024-01-01T00:00:00+00:00", + updated_at="2024-06-01T00:00:00+00:00", + ) + assert rec.id == "custom-id" + assert rec.user_id == sample_user_id + assert rec.thread_id == sample_thread_id + assert rec.role == "agent" + assert rec.memory_type == "summary" + assert rec.content == "summary content" + assert rec.metadata == {"key": "value"} + assert rec.embedding == sample_embedding + assert rec.agent_id == "agent-1" + assert rec.updated_at == "2024-06-01T00:00:00+00:00" + + +# --------------------------------------------------------------------------- +# Role validation +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize("role", ["user", "agent", "tool", "system"]) +def test_valid_roles(role): + rec = MemoryRecord(user_id="u", role=role, content="c") + assert rec.role == role + + +def test_invalid_role(): + with pytest.raises(pydantic.ValidationError, match="role"): + MemoryRecord(user_id="u", role="invalid_role", content="c") + + +# --------------------------------------------------------------------------- +# MemoryType validation +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize("mt", ["turn", "summary", "fact", "user_summary"]) +def test_valid_memory_types(mt): + rec = MemoryRecord(user_id="u", role="user", content="c", memory_type=mt) + assert rec.memory_type == mt + + +def test_invalid_memory_type(): + with pytest.raises(pydantic.ValidationError, match="type"): + MemoryRecord(user_id="u", role="user", content="c", memory_type="bad") + + +# --------------------------------------------------------------------------- +# to_cosmos_dict +# --------------------------------------------------------------------------- + + +def test_to_cosmos_dict_uses_type_key(): + rec = MemoryRecord(user_id="u", role="user", content="c") + d = rec.to_cosmos_dict() + assert "type" in d + assert "memory_type" not in d + assert d["type"] == "turn" + + +def test_to_cosmos_dict_omits_none(): + rec = MemoryRecord(user_id="u", role="user", content="c") + d = rec.to_cosmos_dict() + assert "embedding" not in d + assert "agent_id" not in d + assert "updated_at" not in d + + +def test_to_cosmos_dict_includes_optional_fields(sample_embedding): + rec = MemoryRecord( + user_id="u", + role="user", + content="c", + embedding=sample_embedding, + agent_id="a1", + updated_at="2024-06-01T00:00:00+00:00", + ) + d = rec.to_cosmos_dict() + assert d["embedding"] == sample_embedding + assert d["agent_id"] == "a1" + assert d["updated_at"] == "2024-06-01T00:00:00+00:00" + + +# --------------------------------------------------------------------------- +# from_cosmos_dict round-trip +# --------------------------------------------------------------------------- + + +def test_from_cosmos_dict_round_trip(sample_embedding): + original = MemoryRecord( + id="rt-id", + user_id="u", + thread_id="t", + role="agent", + memory_type="fact", + content="a fact", + metadata={"k": 1}, + embedding=sample_embedding, + agent_id="ag", + created_at="2024-01-01T00:00:00+00:00", + updated_at="2024-06-01T00:00:00+00:00", + ) + cosmos = original.to_cosmos_dict() + restored = MemoryRecord.from_cosmos_dict(cosmos) + assert restored.id == original.id + assert restored.user_id == original.user_id + assert restored.thread_id == original.thread_id + assert restored.role == original.role + assert restored.memory_type == original.memory_type + assert restored.content == original.content + assert restored.metadata == original.metadata + assert restored.embedding == original.embedding + assert restored.agent_id == original.agent_id + assert restored.created_at == original.created_at + assert restored.updated_at == original.updated_at + + +def test_from_cosmos_dict_ignores_system_fields(sample_memory_dict): + doc = {**sample_memory_dict, "_rid": "abc", "_ts": 123, "_etag": "e", "_self": "s"} + rec = MemoryRecord.from_cosmos_dict(doc) + assert rec.id == sample_memory_dict["id"] + assert rec.content == sample_memory_dict["content"] + + +def test_from_cosmos_dict_handles_type_alias(): + doc = { + "id": "x", + "user_id": "u", + "thread_id": "t", + "role": "user", + "type": "summary", + "content": "c", + "metadata": {}, + "created_at": "2024-01-01T00:00:00+00:00", + } + rec = MemoryRecord.from_cosmos_dict(doc) + assert rec.memory_type == "summary" + + +# --------------------------------------------------------------------------- +# SearchResult / OrchestrationResult +# --------------------------------------------------------------------------- + + +def test_search_result(): + rec = MemoryRecord(user_id="u", role="user", content="c") + sr = SearchResult(record=rec, score=0.95) + assert sr.record is rec + assert sr.score == 0.95 + + sr_no_score = SearchResult(record=rec) + assert sr_no_score.score is None + + +def test_orchestration_result(): + orch = OrchestrationResult( + runtime_status="Completed", + output={"result": 42}, + custom_status="done", + instance_id="inst-1", + ) + assert orch.runtime_status == "Completed" + assert orch.output == {"result": 42} + assert orch.custom_status == "done" + assert orch.instance_id == "inst-1" diff --git a/tests/unit/test_processing.py b/tests/unit/test_processing.py new file mode 100644 index 0000000..c3930f3 --- /dev/null +++ b/tests/unit/test_processing.py @@ -0,0 +1,214 @@ +"""Unit tests for ProcessingClient (sync Durable Functions client).""" + +from __future__ import annotations + +import json +from unittest.mock import MagicMock, patch + +import pytest + +from agent_memory_toolkit.exceptions import ( + ConfigurationError, + OrchestrationTimeoutError, + ProcessingError, +) +from agent_memory_toolkit.processing import ProcessingClient + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +FAKE_ENDPOINT = "https://myfunc.azurewebsites.net" + + +def _make_client(**overrides) -> ProcessingClient: + defaults = dict(endpoint=FAKE_ENDPOINT, key=None, poll_interval=0.05, timeout=5.0) + defaults.update(overrides) + return ProcessingClient(**defaults) + + +def _urlopen_response(body: dict) -> MagicMock: + """Create a context-manager mock that returns *body* as JSON bytes.""" + data = json.dumps(body).encode("utf-8") + resp = MagicMock() + resp.read.return_value = data + resp.__enter__ = MagicMock(return_value=resp) + resp.__exit__ = MagicMock(return_value=False) + return resp + + +# --------------------------------------------------------------------------- +# invoke_orchestrator() +# --------------------------------------------------------------------------- + + +class TestInvokeOrchestrator: + @patch("urllib.request.urlopen") + def test_immediate_completion(self, mock_urlopen): + start_resp = _urlopen_response( + {"id": "inst-1", "statusQueryGetUri": "https://status/1"} + ) + poll_resp = _urlopen_response( + {"runtimeStatus": "Completed", "output": {"ok": True}} + ) + mock_urlopen.side_effect = [start_resp, poll_resp] + + client = _make_client() + result = client.invoke_orchestrator({"key": "value"}) + + assert result["runtimeStatus"] == "Completed" + assert result["output"] == {"ok": True} + assert mock_urlopen.call_count == 2 + + @patch("urllib.request.urlopen") + def test_polls_multiple_times(self, mock_urlopen): + start_resp = _urlopen_response( + {"id": "inst-2", "statusQueryGetUri": "https://status/2"} + ) + running_resp_1 = _urlopen_response({"runtimeStatus": "Running"}) + running_resp_2 = _urlopen_response({"runtimeStatus": "Running"}) + done_resp = _urlopen_response( + {"runtimeStatus": "Completed", "output": "done"} + ) + mock_urlopen.side_effect = [ + start_resp, + running_resp_1, + running_resp_2, + done_resp, + ] + + client = _make_client() + result = client.invoke_orchestrator({"x": 1}) + + assert result["runtimeStatus"] == "Completed" + assert mock_urlopen.call_count == 4 + + @patch("urllib.request.urlopen") + def test_failed_status(self, mock_urlopen): + start_resp = _urlopen_response( + {"id": "inst-3", "statusQueryGetUri": "https://status/3"} + ) + fail_resp = _urlopen_response( + {"runtimeStatus": "Failed", "output": "something broke"} + ) + mock_urlopen.side_effect = [start_resp, fail_resp] + + client = _make_client() + with pytest.raises(ProcessingError, match="something broke"): + client.invoke_orchestrator({"y": 2}) + + @patch("urllib.request.urlopen") + def test_timeout(self, mock_urlopen): + start_resp = _urlopen_response( + {"id": "inst-4", "statusQueryGetUri": "https://status/4"} + ) + # Always return Running to force timeout + running_resp = _urlopen_response({"runtimeStatus": "Running"}) + mock_urlopen.side_effect = [start_resp] + [running_resp] * 100 + + client = _make_client(timeout=0.1, poll_interval=0.05) + with pytest.raises(OrchestrationTimeoutError): + client.invoke_orchestrator({"z": 3}) + + def test_missing_endpoint(self): + client = ProcessingClient(endpoint=None) + with pytest.raises(ConfigurationError) as exc_info: + client.invoke_orchestrator({"a": 1}) + assert exc_info.value.parameter == "endpoint" + + @patch("urllib.request.urlopen") + def test_function_key_appended(self, mock_urlopen): + start_resp = _urlopen_response({"runtimeStatus": "Completed"}) + mock_urlopen.side_effect = [start_resp] + + client = _make_client(key="my-secret-key") + client.invoke_orchestrator({"b": 2}) + + # Inspect the URL used in the POST request + req_obj = mock_urlopen.call_args_list[0][0][0] + assert "?code=my-secret-key" in req_obj.full_url + + @patch("urllib.request.urlopen") + def test_no_status_uri_returns_start_response(self, mock_urlopen): + # When no statusQueryGetUri is present, return the start response directly + start_resp = _urlopen_response( + {"runtimeStatus": "Completed", "output": "immediate"} + ) + mock_urlopen.side_effect = [start_resp] + + client = _make_client() + result = client.invoke_orchestrator({"c": 3}) + + assert result["output"] == "immediate" + assert mock_urlopen.call_count == 1 + + +# --------------------------------------------------------------------------- +# Convenience wrappers +# --------------------------------------------------------------------------- + + +class TestGenerateThreadSummary: + @patch("urllib.request.urlopen") + def test_payload_has_thread_summary_flag(self, mock_urlopen): + start_resp = _urlopen_response( + {"id": "ts-1", "statusQueryGetUri": "https://status/ts-1"} + ) + done_resp = _urlopen_response( + {"runtimeStatus": "Completed", "output": "summary"} + ) + mock_urlopen.side_effect = [start_resp, done_resp] + + client = _make_client() + result = client.generate_thread_summary(user_id="u1", thread_id="t1") + + # Verify the POST body + req_obj = mock_urlopen.call_args_list[0][0][0] + body = json.loads(req_obj.data.decode("utf-8")) + assert body["thread_summary_only"] is True + assert body["user_id"] == "u1" + assert body["thread_id"] == "t1" + assert result["runtimeStatus"] == "Completed" + + +class TestExtractFacts: + @patch("urllib.request.urlopen") + def test_payload_has_extract_facts_flag(self, mock_urlopen): + start_resp = _urlopen_response( + {"id": "ef-1", "statusQueryGetUri": "https://status/ef-1"} + ) + done_resp = _urlopen_response( + {"runtimeStatus": "Completed", "output": "facts"} + ) + mock_urlopen.side_effect = [start_resp, done_resp] + + client = _make_client() + result = client.extract_facts(user_id="u1", thread_id="t1") + + req_obj = mock_urlopen.call_args_list[0][0][0] + body = json.loads(req_obj.data.decode("utf-8")) + assert body["extract_facts_only"] is True + assert result["runtimeStatus"] == "Completed" + + +class TestGenerateUserSummary: + @patch("urllib.request.urlopen") + def test_payload_has_user_summary_flag_and_thread_ids(self, mock_urlopen): + start_resp = _urlopen_response( + {"id": "us-1", "statusQueryGetUri": "https://status/us-1"} + ) + done_resp = _urlopen_response( + {"runtimeStatus": "Completed", "output": "user_summary"} + ) + mock_urlopen.side_effect = [start_resp, done_resp] + + client = _make_client() + result = client.generate_user_summary( + user_id="u1", thread_ids=["t1", "t2"] + ) + + req_obj = mock_urlopen.call_args_list[0][0][0] + body = json.loads(req_obj.data.decode("utf-8")) + assert body["user_summary_only"] is True + assert body["thread_ids"] == ["t1", "t2"] + assert result["runtimeStatus"] == "Completed" diff --git a/tests/unit/test_query_builder.py b/tests/unit/test_query_builder.py new file mode 100644 index 0000000..08d0c47 --- /dev/null +++ b/tests/unit/test_query_builder.py @@ -0,0 +1,69 @@ +"""Unit tests for agent_memory_toolkit._query_builder._QueryBuilder.""" + +from agent_memory_toolkit._query_builder import _QueryBuilder + +# --------------------------------------------------------------------------- +# build_where +# --------------------------------------------------------------------------- + + +def test_no_filters_returns_empty_string(): + qb = _QueryBuilder() + assert qb.build_where() == "" + + +def test_one_filter(): + qb = _QueryBuilder() + qb.add_filter("c.user_id", "@user_id", "u1") + assert qb.build_where() == " WHERE c.user_id = @user_id" + params = qb.get_parameters() + assert len(params) == 1 + assert params[0] == {"name": "@user_id", "value": "u1"} + + +def test_multiple_filters_and_joined(): + qb = _QueryBuilder() + qb.add_filter("c.user_id", "@uid", "u1") + qb.add_filter("c.role", "@role", "agent") + where = qb.build_where() + assert where == " WHERE c.user_id = @uid AND c.role = @role" + assert len(qb.get_parameters()) == 2 + + +# --------------------------------------------------------------------------- +# None handling +# --------------------------------------------------------------------------- + + +def test_none_values_skipped(): + qb = _QueryBuilder() + qb.add_filter("c.user_id", "@uid", None) + assert qb.build_where() == "" + assert qb.get_parameters() == [] + + +def test_mixed_none_and_non_none(): + qb = _QueryBuilder() + qb.add_filter("c.user_id", "@uid", "u1") + qb.add_filter("c.role", "@role", None) + qb.add_filter("c.type", "@type", "turn") + where = qb.build_where() + assert where == " WHERE c.user_id = @uid AND c.type = @type" + params = qb.get_parameters() + assert len(params) == 2 + assert params[0]["value"] == "u1" + assert params[1]["value"] == "turn" + + +# --------------------------------------------------------------------------- +# get_parameters returns a copy +# --------------------------------------------------------------------------- + + +def test_get_parameters_returns_copy(): + qb = _QueryBuilder() + qb.add_filter("c.x", "@x", 1) + p1 = qb.get_parameters() + p1.append({"name": "@extra", "value": 99}) + p2 = qb.get_parameters() + assert len(p2) == 1 # original unchanged diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..22e057c --- /dev/null +++ b/uv.lock @@ -0,0 +1,1575 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "agent-memory-toolkit" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "aiohttp" }, + { name = "azure-cosmos" }, + { name = "azure-identity" }, + { name = "openai" }, + { name = "pydantic" }, +] + +[package.optional-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiohttp", specifier = ">=3.9" }, + { name = "azure-cosmos", specifier = ">=4.7" }, + { name = "azure-identity", specifier = ">=1.17" }, + { name = "openai", specifier = ">=1.40" }, + { name = "pydantic", specifier = ">=2.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0" }, + { name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.12" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.4" }, +] +provides-extras = ["dev"] + +[[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.5" +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/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/f5/a20c4ac64aeaef1679e25c9983573618ff765d7aa829fa2b84ae7573169e/aiohttp-3.13.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ab7229b6f9b5c1ba4910d6c41a9eb11f543eadb3f384df1b4c293f4e73d44d6", size = 757513, upload-time = "2026-03-31T21:57:02.146Z" }, + { url = "https://files.pythonhosted.org/packages/75/0a/39fa6c6b179b53fcb3e4b3d2b6d6cad0180854eda17060c7218540102bef/aiohttp-3.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f14c50708bb156b3a3ca7230b3d820199d56a48e3af76fa21c2d6087190fe3d", size = 506748, upload-time = "2026-03-31T21:57:04.275Z" }, + { url = "https://files.pythonhosted.org/packages/87/ec/e38ce072e724fd7add6243613f8d1810da084f54175353d25ccf9f9c7e5a/aiohttp-3.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e7d2f8616f0ff60bd332022279011776c3ac0faa0f1b463f7bb12326fbc97a1c", size = 501673, upload-time = "2026-03-31T21:57:06.208Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ba/3bc7525d7e2beaa11b309a70d48b0d3cfc3c2089ec6a7d0820d59c657053/aiohttp-3.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2567b72e1ffc3ab25510db43f355b29eeada56c0a622e58dcdb19530eb0a3cb", size = 1763757, upload-time = "2026-03-31T21:57:07.882Z" }, + { url = "https://files.pythonhosted.org/packages/5e/ab/e87744cf18f1bd78263aba24924d4953b41086bd3a31d22452378e9028a0/aiohttp-3.13.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fb0540c854ac9c0c5ad495908fdfd3e332d553ec731698c0e29b1877ba0d2ec6", size = 1720152, upload-time = "2026-03-31T21:57:09.946Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f3/ed17a6f2d742af17b50bae2d152315ed1b164b07a5fd5cc1754d99e4dfa5/aiohttp-3.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9883051c6972f58bfc4ebb2116345ee2aa151178e99c3f2b2bbe2af712abd13", size = 1818010, upload-time = "2026-03-31T21:57:12.157Z" }, + { url = "https://files.pythonhosted.org/packages/53/06/ecbc63dc937192e2a5cb46df4d3edb21deb8225535818802f210a6ea5816/aiohttp-3.13.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2294172ce08a82fb7c7273485895de1fa1186cc8294cfeb6aef4af42ad261174", size = 1907251, upload-time = "2026-03-31T21:57:14.023Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a5/0521aa32c1ddf3aa1e71dcc466be0b7db2771907a13f18cddaa45967d97b/aiohttp-3.13.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3a807cabd5115fb55af198b98178997a5e0e57dead43eb74a93d9c07d6d4a7dc", size = 1759969, upload-time = "2026-03-31T21:57:16.146Z" }, + { url = "https://files.pythonhosted.org/packages/f6/78/a38f8c9105199dd3b9706745865a8a59d0041b6be0ca0cc4b2ccf1bab374/aiohttp-3.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6d0d932e0f39c02b80744273cd5c388a2d9bc07760a03164f229c8e02662f6", size = 1616871, upload-time = "2026-03-31T21:57:17.856Z" }, + { url = "https://files.pythonhosted.org/packages/6f/41/27392a61ead8ab38072105c71aa44ff891e71653fe53d576a7067da2b4e8/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60869c7ac4aaabe7110f26499f3e6e5696eae98144735b12a9c3d9eae2b51a49", size = 1739844, upload-time = "2026-03-31T21:57:19.679Z" }, + { url = "https://files.pythonhosted.org/packages/6e/55/5564e7ae26d94f3214250009a0b1c65a0c6af4bf88924ccb6fdab901de28/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26d2f8546f1dfa75efa50c3488215a903c0168d253b75fba4210f57ab77a0fb8", size = 1731969, upload-time = "2026-03-31T21:57:22.006Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c5/705a3929149865fc941bcbdd1047b238e4a72bcb215a9b16b9d7a2e8d992/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1162a1492032c82f14271e831c8f4b49f2b6078f4f5fc74de2c912fa225d51d", size = 1795193, upload-time = "2026-03-31T21:57:24.256Z" }, + { url = "https://files.pythonhosted.org/packages/a6/19/edabed62f718d02cff7231ca0db4ef1c72504235bc467f7b67adb1679f48/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8b14eb3262fad0dc2f89c1a43b13727e709504972186ff6a99a3ecaa77102b6c", size = 1606477, upload-time = "2026-03-31T21:57:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/de/fc/76f80ef008675637d88d0b21584596dc27410a990b0918cb1e5776545b5b/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ca9ac61ac6db4eb6c2a0cd1d0f7e1357647b638ccc92f7e9d8d133e71ed3c6ac", size = 1813198, upload-time = "2026-03-31T21:57:28.316Z" }, + { url = "https://files.pythonhosted.org/packages/e5/67/5b3ac26b80adb20ea541c487f73730dc8fa107d632c998f25bbbab98fcda/aiohttp-3.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7996023b2ed59489ae4762256c8516df9820f751cf2c5da8ed2fb20ee50abab3", size = 1752321, upload-time = "2026-03-31T21:57:30.549Z" }, + { url = "https://files.pythonhosted.org/packages/88/06/e4a2e49255ea23fa4feeb5ab092d90240d927c15e47b5b5c48dff5a9ce29/aiohttp-3.13.5-cp311-cp311-win32.whl", hash = "sha256:77dfa48c9f8013271011e51c00f8ada19851f013cde2c48fca1ba5e0caf5bb06", size = 439069, upload-time = "2026-03-31T21:57:32.388Z" }, + { url = "https://files.pythonhosted.org/packages/c0/43/8c7163a596dab4f8be12c190cf467a1e07e4734cf90eebb39f7f5d53fc6a/aiohttp-3.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:d3a4834f221061624b8887090637db9ad4f61752001eae37d56c52fddade2dc8", size = 462859, upload-time = "2026-03-31T21:57:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, + { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, + { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, + { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, + { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, + { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, + { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, + { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, + { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, + { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, + { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, + { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, + { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, + { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, + { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, + { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, + { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, + { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, + { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, + { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, + { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, + { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, + { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, + { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, + { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, + { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, + { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, + { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, + { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, + { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, + { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, + { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, + { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, + { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, + { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, + { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, + { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "azure-core" +version = "1.39.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/83/bbde3faa84ddcb8eb0eca4b3ffb3221252281db4ce351300fe248c5c70b1/azure_core-1.39.0.tar.gz", hash = "sha256:8a90a562998dd44ce84597590fff6249701b98c0e8797c95fcdd695b54c35d74", size = 367531, upload-time = "2026-03-19T01:31:29.461Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d6/8ebcd05b01a580f086ac9a97fb9fac65c09a4b012161cc97c21a336e880b/azure_core-1.39.0-py3-none-any.whl", hash = "sha256:4ac7b70fab5438c3f68770649a78daf97833caa83827f91df9c14e0e0ea7d34f", size = 218318, upload-time = "2026-03-19T01:31:31.25Z" }, +] + +[[package]] +name = "azure-cosmos" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/a3/0474e622bf9676e3206d61269461ed16a05958363c254ea3b15af16219b2/azure_cosmos-4.15.0.tar.gz", hash = "sha256:be1cf49837c197d9da880ec47fe020a24d679075b89e0e1e2aca8d376b3a5a24", size = 2100744, upload-time = "2026-02-23T16:01:52.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/5f/b6e3d3ae16fa121fdc17e62447800d378b7e716cd6103c3650977a6c4618/azure_cosmos-4.15.0-py3-none-any.whl", hash = "sha256:83c1da7386bcd0df9a15c52116cc35012225d8a72d4f1379938b83ea5eb19fff", size = 424870, upload-time = "2026-02-23T16:01:54.514Z" }, +] + +[[package]] +name = "azure-identity" +version = "1.25.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "cryptography" }, + { name = "msal" }, + { name = "msal-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/0e/3a63efb48aa4a5ae2cfca61ee152fbcb668092134d3eb8bfda472dd5c617/azure_identity-1.25.3.tar.gz", hash = "sha256:ab23c0d63015f50b630ef6c6cf395e7262f439ce06e5d07a64e874c724f8d9e6", size = 286304, upload-time = "2026-03-13T01:12:20.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/9a/417b3a533e01953a7c618884df2cb05a71e7b68bdbce4fbdb62349d2a2e8/azure_identity-1.25.3-py3-none-any.whl", hash = "sha256:f4d0b956a8146f30333e071374171f3cfa7bdb8073adb8c3814b65567aa7447c", size = 192138, upload-time = "2026-03-13T01:12:22.951Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, + { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" }, + { url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" }, + { url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" }, + { url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" }, + { url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a", size = 250080, upload-time = "2026-03-17T10:30:29.481Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" }, + { url = "https://files.pythonhosted.org/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf", size = 221880, upload-time = "2026-03-17T10:30:36.775Z" }, + { url = "https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9", size = 222816, upload-time = "2026-03-17T10:30:38.891Z" }, + { url = "https://files.pythonhosted.org/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028", size = 221483, upload-time = "2026-03-17T10:30:40.463Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, + { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, + { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, + { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "46.0.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542, upload-time = "2026-03-25T23:34:53.396Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/23/9285e15e3bc57325b0a72e592921983a701efc1ee8f91c06c5f0235d86d9/cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8", size = 7176401, upload-time = "2026-03-25T23:33:22.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size = 4275275, upload-time = "2026-03-25T23:33:23.844Z" }, + { url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size = 4425320, upload-time = "2026-03-25T23:33:25.703Z" }, + { url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size = 4278082, upload-time = "2026-03-25T23:33:27.423Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size = 4926514, upload-time = "2026-03-25T23:33:29.206Z" }, + { url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size = 4457766, upload-time = "2026-03-25T23:33:30.834Z" }, + { url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size = 3986535, upload-time = "2026-03-25T23:33:33.02Z" }, + { url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size = 4277618, upload-time = "2026-03-25T23:33:34.567Z" }, + { url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size = 4890802, upload-time = "2026-03-25T23:33:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size = 4457425, upload-time = "2026-03-25T23:33:38.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size = 4405530, upload-time = "2026-03-25T23:33:40.842Z" }, + { url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size = 4667896, upload-time = "2026-03-25T23:33:42.781Z" }, + { url = "https://files.pythonhosted.org/packages/ad/b5/1895bc0821226f129bc74d00eccfc6a5969e2028f8617c09790bf89c185e/cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2", size = 3026348, upload-time = "2026-03-25T23:33:45.021Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f8/c9bcbf0d3e6ad288b9d9aa0b1dee04b063d19e8c4f871855a03ab3a297ab/cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124", size = 3483896, upload-time = "2026-03-25T23:33:46.649Z" }, + { url = "https://files.pythonhosted.org/packages/01/41/3a578f7fd5c70611c0aacba52cd13cb364a5dee895a5c1d467208a9380b0/cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275", size = 7117147, upload-time = "2026-03-25T23:33:48.249Z" }, + { url = "https://files.pythonhosted.org/packages/fa/87/887f35a6fca9dde90cad08e0de0c89263a8e59b2d2ff904fd9fcd8025b6f/cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4", size = 4266221, upload-time = "2026-03-25T23:33:49.874Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a8/0a90c4f0b0871e0e3d1ed126aed101328a8a57fd9fd17f00fb67e82a51ca/cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b", size = 4408952, upload-time = "2026-03-25T23:33:52.128Z" }, + { url = "https://files.pythonhosted.org/packages/16/0b/b239701eb946523e4e9f329336e4ff32b1247e109cbab32d1a7b61da8ed7/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707", size = 4270141, upload-time = "2026-03-25T23:33:54.11Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a8/976acdd4f0f30df7b25605f4b9d3d89295351665c2091d18224f7ad5cdbf/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361", size = 4904178, upload-time = "2026-03-25T23:33:55.725Z" }, + { url = "https://files.pythonhosted.org/packages/b1/1b/bf0e01a88efd0e59679b69f42d4afd5bced8700bb5e80617b2d63a3741af/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b", size = 4441812, upload-time = "2026-03-25T23:33:57.364Z" }, + { url = "https://files.pythonhosted.org/packages/bb/8b/11df86de2ea389c65aa1806f331cae145f2ed18011f30234cc10ca253de8/cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca", size = 3963923, upload-time = "2026-03-25T23:33:59.361Z" }, + { url = "https://files.pythonhosted.org/packages/91/e0/207fb177c3a9ef6a8108f234208c3e9e76a6aa8cf20d51932916bd43bda0/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013", size = 4269695, upload-time = "2026-03-25T23:34:00.909Z" }, + { url = "https://files.pythonhosted.org/packages/21/5e/19f3260ed1e95bced52ace7501fabcd266df67077eeb382b79c81729d2d3/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4", size = 4869785, upload-time = "2026-03-25T23:34:02.796Z" }, + { url = "https://files.pythonhosted.org/packages/10/38/cd7864d79aa1d92ef6f1a584281433419b955ad5a5ba8d1eb6c872165bcb/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a", size = 4441404, upload-time = "2026-03-25T23:34:04.35Z" }, + { url = "https://files.pythonhosted.org/packages/09/0a/4fe7a8d25fed74419f91835cf5829ade6408fd1963c9eae9c4bce390ecbb/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d", size = 4397549, upload-time = "2026-03-25T23:34:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a0/7d738944eac6513cd60a8da98b65951f4a3b279b93479a7e8926d9cd730b/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736", size = 4651874, upload-time = "2026-03-25T23:34:07.916Z" }, + { url = "https://files.pythonhosted.org/packages/cb/f1/c2326781ca05208845efca38bf714f76939ae446cd492d7613808badedf1/cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed", size = 3001511, upload-time = "2026-03-25T23:34:09.892Z" }, + { url = "https://files.pythonhosted.org/packages/c9/57/fe4a23eb549ac9d903bd4698ffda13383808ef0876cc912bcb2838799ece/cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4", size = 3471692, upload-time = "2026-03-25T23:34:11.613Z" }, + { url = "https://files.pythonhosted.org/packages/c4/cc/f330e982852403da79008552de9906804568ae9230da8432f7496ce02b71/cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a", size = 7162776, upload-time = "2026-03-25T23:34:13.308Z" }, + { url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size = 4270529, upload-time = "2026-03-25T23:34:15.019Z" }, + { url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size = 4414827, upload-time = "2026-03-25T23:34:16.604Z" }, + { url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size = 4271265, upload-time = "2026-03-25T23:34:18.586Z" }, + { url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size = 4916800, upload-time = "2026-03-25T23:34:20.561Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size = 4448771, upload-time = "2026-03-25T23:34:22.406Z" }, + { url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size = 3978333, upload-time = "2026-03-25T23:34:24.281Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size = 4271069, upload-time = "2026-03-25T23:34:25.895Z" }, + { url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size = 4878358, upload-time = "2026-03-25T23:34:27.67Z" }, + { url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size = 4448061, upload-time = "2026-03-25T23:34:29.375Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size = 4399103, upload-time = "2026-03-25T23:34:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size = 4659255, upload-time = "2026-03-25T23:34:33.679Z" }, + { url = "https://files.pythonhosted.org/packages/0a/09/ddc5f630cc32287d2c953fc5d32705e63ec73e37308e5120955316f53827/cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c", size = 3010660, upload-time = "2026-03-25T23:34:35.418Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size = 3471160, upload-time = "2026-03-25T23:34:37.191Z" }, + { url = "https://files.pythonhosted.org/packages/2e/84/7ccff00ced5bac74b775ce0beb7d1be4e8637536b522b5df9b73ada42da2/cryptography-46.0.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:2ea0f37e9a9cf0df2952893ad145fd9627d326a59daec9b0802480fa3bcd2ead", size = 3475444, upload-time = "2026-03-25T23:34:38.944Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1f/4c926f50df7749f000f20eede0c896769509895e2648db5da0ed55db711d/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a3e84d5ec9ba01f8fd03802b2147ba77f0c8f2617b2aff254cedd551844209c8", size = 4218227, upload-time = "2026-03-25T23:34:40.871Z" }, + { url = "https://files.pythonhosted.org/packages/c6/65/707be3ffbd5f786028665c3223e86e11c4cda86023adbc56bd72b1b6bab5/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:12f0fa16cc247b13c43d56d7b35287ff1569b5b1f4c5e87e92cc4fcc00cd10c0", size = 4381399, upload-time = "2026-03-25T23:34:42.609Z" }, + { url = "https://files.pythonhosted.org/packages/f3/6d/73557ed0ef7d73d04d9aba745d2c8e95218213687ee5e76b7d236a5030fc/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:50575a76e2951fe7dbd1f56d181f8c5ceeeb075e9ff88e7ad997d2f42af06e7b", size = 4217595, upload-time = "2026-03-25T23:34:44.205Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c5/e1594c4eec66a567c3ac4400008108a415808be2ce13dcb9a9045c92f1a0/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:90e5f0a7b3be5f40c3a0a0eafb32c681d8d2c181fc2a1bdabe9b3f611d9f6b1a", size = 4380912, upload-time = "2026-03-25T23:34:46.328Z" }, + { url = "https://files.pythonhosted.org/packages/1a/89/843b53614b47f97fe1abc13f9a86efa5ec9e275292c457af1d4a60dc80e0/cryptography-46.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6728c49e3b2c180ef26f8e9f0a883a2c585638db64cf265b49c9ba10652d430e", size = 3409955, upload-time = "2026-03-25T23:34:48.465Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jiter" +version = "0.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" }, + { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" }, + { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" }, + { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" }, + { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" }, + { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" }, + { url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709, upload-time = "2026-02-02T12:35:52.467Z" }, + { url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560, upload-time = "2026-02-02T12:35:53.925Z" }, + { url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608, upload-time = "2026-02-02T12:35:55.304Z" }, + { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, + { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, + { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, + { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, + { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, + { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, + { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, + { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, + { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, + { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, + { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, + { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, + { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, + { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, + { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, + { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, + { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, + { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, + { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, + { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, + { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, + { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, + { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, + { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, + { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, + { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, + { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, + { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, + { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, + { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, + { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, + { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, + { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, + { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" }, + { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" }, + { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" }, + { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, + { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, +] + +[[package]] +name = "msal" +version = "1.35.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/aa/5a646093ac218e4a329391d5a31e5092a89db7d2ef1637a90b82cd0b6f94/msal-1.35.1.tar.gz", hash = "sha256:70cac18ab80a053bff86219ba64cfe3da1f307c74b009e2da57ef040eb1b5656", size = 165658, upload-time = "2026-03-04T23:38:51.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/86/16815fddf056ca998853c6dc525397edf0b43559bb4073a80d2bc7fe8009/msal-1.35.1-py3-none-any.whl", hash = "sha256:8f4e82f34b10c19e326ec69f44dc6b30171f2f7098f3720ea8a9f0c11832caa3", size = 119909, upload-time = "2026-03-04T23:38:50.452Z" }, +] + +[[package]] +name = "msal-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315, upload-time = "2025-03-14T23:51:03.902Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583, upload-time = "2025-03-14T23:51:03.016Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/f1/a90635c4f88fb913fbf4ce660b83b7445b7a02615bda034b2f8eb38fd597/multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d", size = 76626, upload-time = "2026-01-26T02:43:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/267e64eaf6fc637a15b35f5de31a566634a2740f97d8d094a69d34f524a4/multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e", size = 44706, upload-time = "2026-01-26T02:43:27.607Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a4/d45caf2b97b035c57267791ecfaafbd59c68212004b3842830954bb4b02e/multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855", size = 44356, upload-time = "2026-01-26T02:43:28.661Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d2/0a36c8473f0cbaeadd5db6c8b72d15bbceeec275807772bfcd059bef487d/multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3", size = 244355, upload-time = "2026-01-26T02:43:31.165Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/8c65be997fd7dd311b7d39c7b6e71a0cb449bad093761481eccbbe4b42a2/multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e", size = 246433, upload-time = "2026-01-26T02:43:32.581Z" }, + { url = "https://files.pythonhosted.org/packages/01/fb/4dbd7e848d2799c6a026ec88ad39cf2b8416aa167fcc903baa55ecaa045c/multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a", size = 225376, upload-time = "2026-01-26T02:43:34.417Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4a3a6341eac3830f6053062f8fbc9a9e54407c80755b3f05bc427295c2d0/multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8", size = 257365, upload-time = "2026-01-26T02:43:35.741Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a2/dd575a69c1aa206e12d27d0770cdf9b92434b48a9ef0cd0d1afdecaa93c4/multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0", size = 254747, upload-time = "2026-01-26T02:43:36.976Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/21b27c560c13822ed93133f08aa6372c53a8e067f11fbed37b4adcdac922/multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144", size = 246293, upload-time = "2026-01-26T02:43:38.258Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/23466059dc3854763423d0ad6c0f3683a379d97673b1b89ec33826e46728/multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49", size = 242962, upload-time = "2026-01-26T02:43:40.034Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/51dd754a3524d685958001e8fa20a0f5f90a6a856e0a9dcabff69be3dbb7/multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71", size = 237360, upload-time = "2026-01-26T02:43:41.752Z" }, + { url = "https://files.pythonhosted.org/packages/64/3f/036dfc8c174934d4b55d86ff4f978e558b0e585cef70cfc1ad01adc6bf18/multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3", size = 245940, upload-time = "2026-01-26T02:43:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/3d/20/6214d3c105928ebc353a1c644a6ef1408bc5794fcb4f170bb524a3c16311/multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c", size = 253502, upload-time = "2026-01-26T02:43:44.371Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e2/c653bc4ae1be70a0f836b82172d643fcf1dade042ba2676ab08ec08bff0f/multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0", size = 247065, upload-time = "2026-01-26T02:43:45.745Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/a854b4154cd3bd8b1fd375e8a8ca9d73be37610c361543d56f764109509b/multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa", size = 241870, upload-time = "2026-01-26T02:43:47.054Z" }, + { url = "https://files.pythonhosted.org/packages/13/bf/9676c0392309b5fdae322333d22a829715b570edb9baa8016a517b55b558/multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a", size = 41302, upload-time = "2026-01-26T02:43:48.753Z" }, + { url = "https://files.pythonhosted.org/packages/c9/68/f16a3a8ba6f7b6dc92a1f19669c0810bd2c43fc5a02da13b1cbf8e253845/multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b", size = 45981, upload-time = "2026-01-26T02:43:49.921Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/9dd5305253fa00cd3c7555dbef69d5bf4133debc53b87ab8d6a44d411665/multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6", size = 43159, upload-time = "2026-01-26T02:43:51.635Z" }, + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "openai" +version = "2.30.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/15/52580c8fbc16d0675d516e8749806eda679b16de1e4434ea06fb6feaa610/openai-2.30.0.tar.gz", hash = "sha256:92f7661c990bda4b22a941806c83eabe4896c3094465030dd882a71abe80c885", size = 676084, upload-time = "2026-03-25T22:08:59.96Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/9e/5bfa2270f902d5b92ab7d41ce0475b8630572e71e349b2a4996d14bdda93/openai-2.30.0-py3-none-any.whl", hash = "sha256:9a5ae616888eb2748ec5e0c5b955a51592e0b201a11f4262db920f2a78c5231d", size = 1146656, upload-time = "2026-03-25T22:08:58.2Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, +] + +[[package]] +name = "requests" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/97/e9f1ca355108ef7194e38c812ef40ba98c7208f47b13ad78d023caa583da/ruff-0.15.9.tar.gz", hash = "sha256:29cbb1255a9797903f6dde5ba0188c707907ff44a9006eb273b5a17bfa0739a2", size = 4617361, upload-time = "2026-04-02T18:17:20.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/1f/9cdfd0ac4b9d1e5a6cf09bedabdf0b56306ab5e333c85c87281273e7b041/ruff-0.15.9-py3-none-linux_armv6l.whl", hash = "sha256:6efbe303983441c51975c243e26dff328aca11f94b70992f35b093c2e71801e1", size = 10511206, upload-time = "2026-04-02T18:16:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/3d/f6/32bfe3e9c136b35f02e489778d94384118bb80fd92c6d92e7ccd97db12ce/ruff-0.15.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4965bac6ac9ea86772f4e23587746f0b7a395eccabb823eb8bfacc3fa06069f7", size = 10923307, upload-time = "2026-04-02T18:17:08.645Z" }, + { url = "https://files.pythonhosted.org/packages/ca/25/de55f52ab5535d12e7aaba1de37a84be6179fb20bddcbe71ec091b4a3243/ruff-0.15.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf05aad70ca5b5a0a4b0e080df3a6b699803916d88f006efd1f5b46302daab8", size = 10316722, upload-time = "2026-04-02T18:16:44.206Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/690d75f3fd6278fe55fff7c9eb429c92d207e14b25d1cae4064a32677029/ruff-0.15.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9439a342adb8725f32f92732e2bafb6d5246bd7a5021101166b223d312e8fc59", size = 10623674, upload-time = "2026-04-02T18:16:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ec/176f6987be248fc5404199255522f57af1b4a5a1b57727e942479fec98ad/ruff-0.15.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5e6faf9d97c8edc43877c3f406f47446fc48c40e1442d58cfcdaba2acea745", size = 10351516, upload-time = "2026-04-02T18:16:57.206Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fc/51cffbd2b3f240accc380171d51446a32aa2ea43a40d4a45ada67368fbd2/ruff-0.15.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b34a9766aeec27a222373d0b055722900fbc0582b24f39661aa96f3fe6ad901", size = 11150202, upload-time = "2026-04-02T18:17:06.452Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d4/25292a6dfc125f6b6528fe6af31f5e996e19bf73ca8e3ce6eb7fa5b95885/ruff-0.15.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89dd695bc72ae76ff484ae54b7e8b0f6b50f49046e198355e44ea656e521fef9", size = 11988891, upload-time = "2026-04-02T18:17:18.575Z" }, + { url = "https://files.pythonhosted.org/packages/13/e1/1eebcb885c10e19f969dcb93d8413dfee8172578709d7ee933640f5e7147/ruff-0.15.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce187224ef1de1bd225bc9a152ac7102a6171107f026e81f317e4257052916d5", size = 11480576, upload-time = "2026-04-02T18:16:52.986Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/a1548ac378a78332a4c3dcf4a134c2475a36d2a22ddfa272acd574140b50/ruff-0.15.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0c7c341f68adb01c488c3b7d4b49aa8ea97409eae6462d860a79cf55f431b6", size = 11254525, upload-time = "2026-04-02T18:17:02.041Z" }, + { url = "https://files.pythonhosted.org/packages/42/aa/4bb3af8e61acd9b1281db2ab77e8b2c3c5e5599bf2a29d4a942f1c62b8d6/ruff-0.15.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:55cc15eee27dc0eebdfcb0d185a6153420efbedc15eb1d38fe5e685657b0f840", size = 11204072, upload-time = "2026-04-02T18:17:13.581Z" }, + { url = "https://files.pythonhosted.org/packages/69/48/d550dc2aa6e423ea0bcc1d0ff0699325ffe8a811e2dba156bd80750b86dc/ruff-0.15.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6537f6eed5cda688c81073d46ffdfb962a5f29ecb6f7e770b2dc920598997ed", size = 10594998, upload-time = "2026-04-02T18:16:46.369Z" }, + { url = "https://files.pythonhosted.org/packages/63/47/321167e17f5344ed5ec6b0aa2cff64efef5f9e985af8f5622cfa6536043f/ruff-0.15.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6d3fcbca7388b066139c523bda744c822258ebdcfbba7d24410c3f454cc9af71", size = 10359769, upload-time = "2026-04-02T18:17:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/67/5e/074f00b9785d1d2c6f8c22a21e023d0c2c1817838cfca4c8243200a1fa87/ruff-0.15.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:058d8e99e1bfe79d8a0def0b481c56059ee6716214f7e425d8e737e412d69677", size = 10850236, upload-time = "2026-04-02T18:16:48.749Z" }, + { url = "https://files.pythonhosted.org/packages/76/37/804c4135a2a2caf042925d30d5f68181bdbd4461fd0d7739da28305df593/ruff-0.15.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8e1ddb11dbd61d5983fa2d7d6370ef3eb210951e443cace19594c01c72abab4c", size = 11358343, upload-time = "2026-04-02T18:16:55.068Z" }, + { url = "https://files.pythonhosted.org/packages/88/3d/1364fcde8656962782aa9ea93c92d98682b1ecec2f184e625a965ad3b4a6/ruff-0.15.9-py3-none-win32.whl", hash = "sha256:bde6ff36eaf72b700f32b7196088970bf8fdb2b917b7accd8c371bfc0fd573ec", size = 10583382, upload-time = "2026-04-02T18:17:04.261Z" }, + { url = "https://files.pythonhosted.org/packages/4c/56/5c7084299bd2cacaa07ae63a91c6f4ba66edc08bf28f356b24f6b717c799/ruff-0.15.9-py3-none-win_amd64.whl", hash = "sha256:45a70921b80e1c10cf0b734ef09421f71b5aa11d27404edc89d7e8a69505e43d", size = 11744969, upload-time = "2026-04-02T18:16:59.611Z" }, + { url = "https://files.pythonhosted.org/packages/03/36/76704c4f312257d6dbaae3c959add2a622f63fcca9d864659ce6d8d97d3d/ruff-0.15.9-py3-none-win_arm64.whl", hash = "sha256:0694e601c028fd97dc5c6ee244675bc241aeefced7ef80cd9c6935a871078f53", size = 11005870, upload-time = "2026-04-02T18:17:15.773Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "yarl" +version = "1.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/aa/60da938b8f0997ba3a911263c40d82b6f645a67902a490b46f3355e10fae/yarl-1.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99", size = 123641, upload-time = "2026-03-01T22:04:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/24/84/e237607faf4e099dbb8a4f511cfd5efcb5f75918baad200ff7380635631b/yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c", size = 86248, upload-time = "2026-03-01T22:04:44.757Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0d/71ceabc14c146ba8ee3804ca7b3d42b1664c8440439de5214d366fec7d3a/yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432", size = 85988, upload-time = "2026-03-01T22:04:46.365Z" }, + { url = "https://files.pythonhosted.org/packages/8c/6c/4a90d59c572e46b270ca132aca66954f1175abd691f74c1ef4c6711828e2/yarl-1.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a", size = 100566, upload-time = "2026-03-01T22:04:47.639Z" }, + { url = "https://files.pythonhosted.org/packages/49/fb/c438fb5108047e629f6282a371e6e91cf3f97ee087c4fb748a1f32ceef55/yarl-1.23.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05", size = 92079, upload-time = "2026-03-01T22:04:48.925Z" }, + { url = "https://files.pythonhosted.org/packages/d9/13/d269aa1aed3e4f50a5a103f96327210cc5fa5dd2d50882778f13c7a14606/yarl-1.23.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83", size = 108741, upload-time = "2026-03-01T22:04:50.838Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/115b16f22c37ea4437d323e472945bea97301c8ec6089868fa560abab590/yarl-1.23.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c", size = 108099, upload-time = "2026-03-01T22:04:52.499Z" }, + { url = "https://files.pythonhosted.org/packages/9a/64/c53487d9f4968045b8afa51aed7ca44f58b2589e772f32745f3744476c82/yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598", size = 102678, upload-time = "2026-03-01T22:04:55.176Z" }, + { url = "https://files.pythonhosted.org/packages/85/59/cd98e556fbb2bf8fab29c1a722f67ad45c5f3447cac798ab85620d1e70af/yarl-1.23.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b", size = 100803, upload-time = "2026-03-01T22:04:56.588Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c0/b39770b56d4a9f0bb5f77e2f1763cd2d75cc2f6c0131e3b4c360348fcd65/yarl-1.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c", size = 100163, upload-time = "2026-03-01T22:04:58.492Z" }, + { url = "https://files.pythonhosted.org/packages/e7/64/6980f99ab00e1f0ff67cb84766c93d595b067eed07439cfccfc8fb28c1a6/yarl-1.23.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788", size = 93859, upload-time = "2026-03-01T22:05:00.268Z" }, + { url = "https://files.pythonhosted.org/packages/38/69/912e6c5e146793e5d4b5fe39ff5b00f4d22463dfd5a162bec565ac757673/yarl-1.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222", size = 108202, upload-time = "2026-03-01T22:05:02.273Z" }, + { url = "https://files.pythonhosted.org/packages/59/97/35ca6767524687ad64e5f5c31ad54bc76d585585a9fcb40f649e7e82ffed/yarl-1.23.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb", size = 99866, upload-time = "2026-03-01T22:05:03.597Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1c/1a3387ee6d73589f6f2a220ae06f2984f6c20b40c734989b0a44f5987308/yarl-1.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc", size = 107852, upload-time = "2026-03-01T22:05:04.986Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/35c0750fcd5a3f781058bfd954515dd4b1eab45e218cbb85cf11132215f1/yarl-1.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2", size = 102919, upload-time = "2026-03-01T22:05:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1c/9a1979aec4a81896d597bcb2177827f2dbee3f5b7cc48b2d0dadb644b41d/yarl-1.23.0-cp311-cp311-win32.whl", hash = "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", size = 82602, upload-time = "2026-03-01T22:05:08.444Z" }, + { url = "https://files.pythonhosted.org/packages/93/22/b85eca6fa2ad9491af48c973e4c8cf6b103a73dbb271fe3346949449fca0/yarl-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", size = 87461, upload-time = "2026-03-01T22:05:10.145Z" }, + { url = "https://files.pythonhosted.org/packages/93/95/07e3553fe6f113e6864a20bdc53a78113cda3b9ced8784ee52a52c9f80d8/yarl-1.23.0-cp311-cp311-win_arm64.whl", hash = "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", size = 82336, upload-time = "2026-03-01T22:05:11.554Z" }, + { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, + { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, + { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, + { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, + { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, + { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, + { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, + { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, + { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, + { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, + { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, + { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, + { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, + { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, + { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, + { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, + { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, + { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, + { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, + { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, + { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, + { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, + { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, +]